Skip to content

Commit 7221c03

Browse files
committed
Merge branch 'feature/global-chat' into develop
2 parents b5b47a7 + bafe7c0 commit 7221c03

16 files changed

Lines changed: 229 additions & 133 deletions

Core/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ let package = Package(
140140
name: "OpenAIService",
141141
dependencies: [
142142
"Logger",
143+
"Preferences",
143144
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
144145
]
145146
),

Core/Sources/Preferences/Keys.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ public struct UserDefaultPreferenceKeys {
109109
.init()
110110
}
111111

112+
public struct UseGlobalChat: UserDefaultPreferenceKey {
113+
public let defaultValue = false
114+
public let key = "UseGlobalChat"
115+
}
116+
117+
public var useGlobalChat: UseGlobalChat { .init() }
118+
112119
public var disableLazyVStack: FeatureFlags.DisableLazyVStack { .init() }
113120
}
114121

Core/Sources/Service/GUI/ChatProvider+Service.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import OpenAIService
44
import SuggestionWidget
55

66
extension ChatProvider {
7-
convenience init(service: ChatService, fileURL: URL, onCloseChat: @escaping () -> Void) {
7+
convenience init(
8+
service: ChatService,
9+
fileURL: URL,
10+
onCloseChat: @escaping () -> Void,
11+
onSwitchContext: @escaping () -> Void
12+
) {
813
self.init()
914
let cancellable = service.objectWillChange.sink { [weak self] in
1015
guard let self else { return }
@@ -19,7 +24,7 @@ extension ChatProvider {
1924
self.isReceivingMessage = await service.chatGPTService.isReceivingMessage
2025
}
2126
}
22-
27+
2328
service.objectWillChange.send()
2429

2530
onMessageSend = { [cancellable] message in
@@ -47,9 +52,12 @@ extension ChatProvider {
4752
onClose = {
4853
Task {
4954
await service.stopReceivingMessage()
50-
PresentInWindowSuggestionPresenter().closeChatRoom(fileURL: fileURL)
5155
onCloseChat()
5256
}
5357
}
58+
59+
self.onSwitchContext = {
60+
onSwitchContext()
61+
}
5462
}
5563
}

Core/Sources/Service/GUI/WidgetDataSource.swift

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
import ChatService
22
import Foundation
3+
import OpenAIService
34
import SuggestionWidget
45

5-
final class WidgetDataSource: SuggestionWidgetDataSource {
6+
final class WidgetDataSource {
67
static let shared = WidgetDataSource()
78

9+
var globalChat: ChatService?
10+
var chats = [URL: ChatService]()
11+
812
private init() {}
913

14+
@discardableResult
15+
func createChatIfNeeded(for url: URL) -> ChatService {
16+
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
17+
let chat: ChatService
18+
if useGlobalChat {
19+
chat = globalChat ?? ChatService(chatGPTService: ChatGPTService())
20+
globalChat = chat
21+
} else {
22+
chat = chats[url] ?? ChatService(chatGPTService: ChatGPTService())
23+
chats[url] = chat
24+
}
25+
return chat
26+
}
27+
}
28+
29+
extension WidgetDataSource: SuggestionWidgetDataSource {
1030
func suggestionForFile(at url: URL) async -> SuggestionProvider? {
1131
for workspace in await workspaces.values {
1232
if let filespace = await workspace.filespaces[url],
@@ -49,30 +69,40 @@ final class WidgetDataSource: SuggestionWidgetDataSource {
4969
}
5070

5171
func chatForFile(at url: URL) async -> ChatProvider? {
52-
for workspace in await workspaces.values {
53-
if let filespace = await workspace.filespaces[url],
54-
let service = await filespace.chatService
55-
{
56-
return .init(
57-
service: service,
58-
fileURL: url,
59-
onCloseChat: { [weak filespace] in
60-
Task { @ServiceActor [weak filespace] in
61-
filespace?.chatService = nil
62-
}
72+
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
73+
let buildChatProvider = { (service: ChatService) in
74+
ChatProvider(
75+
service: service,
76+
fileURL: url,
77+
onCloseChat: { [weak self] in
78+
if UserDefaults.shared.value(for: \.useGlobalChat) {
79+
self?.globalChat = nil
80+
} else {
81+
self?.chats[url] = nil
6382
}
64-
)
65-
}
83+
let presenter = PresentInWindowSuggestionPresenter()
84+
presenter.closeChatRoom(fileURL: url)
85+
},
86+
onSwitchContext: { [weak self] in
87+
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
88+
UserDefaults.shared.set(!useGlobalChat, for: \.useGlobalChat)
89+
self?.createChatIfNeeded(for: url)
90+
let presenter = PresentInWindowSuggestionPresenter()
91+
presenter.presentChatRoom(fileURL: url)
92+
}
93+
)
6694
}
67-
return nil
68-
}
6995

70-
func chatServiceForFile(at url: URL) async -> ChatService? {
71-
for workspace in await workspaces.values {
72-
if let filespace = await workspace.filespaces[url] {
73-
return await filespace.chatService
96+
if useGlobalChat {
97+
if let globalChat {
98+
return buildChatProvider(globalChat)
99+
}
100+
} else {
101+
if let service = chats[url] {
102+
return buildChatProvider(service)
74103
}
75104
}
105+
76106
return nil
77107
}
78108
}

Core/Sources/Service/ScheduledCleaner.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@ import Foundation
22

33
public final class ScheduledCleaner {
44
public init() {
5-
// Occasionally cleanup workspaces.
5+
// occasionally cleanup workspaces.
66
Task { @ServiceActor in
77
while !Task.isCancelled {
88
try await Task.sleep(nanoseconds: 8 * 60 * 60 * 1_000_000_000)
99
for (url, workspace) in workspaces {
1010
if workspace.isExpired {
1111
workspaces[url] = nil
1212
} else {
13-
workspaces[url]?.cleanUp()
13+
// cleanup chats for unused files
14+
let filespaces = workspace.filespaces
15+
for (url, filespace) in filespaces {
16+
if filespace.isExpired {
17+
WidgetDataSource.shared.chats[url] = nil
18+
}
19+
}
20+
// cleanup workspace
21+
workspace.cleanUp()
1422
}
1523
}
1624
}

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,11 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
188188
defer { presenter.markAsProcessing(false) }
189189

190190
let fileURL = try await Environment.fetchCurrentFileURL()
191-
let (_, filespace) = try await Workspace
192-
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
193191
let language = UserDefaults.shared.value(for: \.chatGPTLanguage)
194192
let codeLanguage = languageIdentifierFromFileURL(fileURL)
195193
guard let selection = editor.selections.last else { return }
196194

197-
let chat = filespace.chatService ?? ChatService(chatGPTService: ChatGPTService())
198-
filespace.chatService = chat
195+
let chat = WidgetDataSource.shared.createChatIfNeeded(for: fileURL)
199196

200197
await chat.mutateSystemPrompt(
201198
"""
@@ -212,11 +209,11 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
212209
\(code)
213210
```
214211
""",
215-
summary: "Explain selected code from `\(selection.start.line + 1):\(selection.start.character + 1)` to `\(selection.end.line + 1):\(selection.end.character + 1)`."
212+
summary: "Explain selected code in `\(fileURL.lastPathComponent)` from `\(selection.start.line + 1):\(selection.start.character + 1)` to `\(selection.end.line + 1):\(selection.end.character + 1)`."
216213
)
217214
}
218215

219-
presenter.presentChatGPTConversation(fileURL: fileURL)
216+
presenter.presentChatRoom(fileURL: fileURL)
220217
}
221218

222219
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent? {
@@ -235,8 +232,6 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
235232
defer { presenter.markAsProcessing(false) }
236233

237234
let fileURL = try await Environment.fetchCurrentFileURL()
238-
let (_, filespace) = try await Workspace
239-
.fetchOrCreateWorkspaceIfNeeded(fileURL: fileURL)
240235
let language = UserDefaults.shared.value(for: \.chatGPTLanguage)
241236
let codeLanguage = languageIdentifierFromFileURL(fileURL)
242237

@@ -262,8 +257,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
262257
"""
263258
}()
264259

265-
let chat = filespace.chatService ?? ChatService(chatGPTService: ChatGPTService())
266-
filespace.chatService = chat
260+
let chat = WidgetDataSource.shared.createChatIfNeeded(for: fileURL)
267261

268262
await chat.mutateSystemPrompt(prompt)
269263

@@ -273,12 +267,12 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
273267
history.append(.init(
274268
role: .user,
275269
content: "",
276-
summary: "Chat about selected code from `\(selection.start.line + 1):\(selection.start.character + 1)` to `\(selection.end.line + 1):\(selection.end.character)`.\nThe code will persist in the conversation."
270+
summary: "Chat about selected code in `\(fileURL.lastPathComponent)` from `\(selection.start.line + 1):\(selection.start.character + 1)` to `\(selection.end.line + 1):\(selection.end.character)`.\nThe code will persist in the conversation."
277271
))
278272
}
279273
}
280274
}
281275

282-
presenter.presentChatGPTConversation(fileURL: fileURL)
276+
presenter.presentChatRoom(fileURL: fileURL)
283277
}
284278
}

Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ struct PresentInWindowSuggestionPresenter {
4848
}
4949
}
5050

51-
func presentChatGPTConversation(fileURL: URL) {
51+
func presentChatRoom(fileURL: URL) {
5252
Task { @MainActor in
5353
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
5454
controller.presentChatRoom(fileURL: fileURL)

Core/Sources/Service/Workspace.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ final class Filespace {
3939
Environment.now().timeIntervalSince(lastSuggestionUpdateTime) > 60 * 60 * 8
4040
}
4141

42-
var chatService: ChatService? = nil
43-
4442
init(fileURL: URL) {
4543
self.fileURL = fileURL
4644
}

Core/Sources/SuggestionWidget/ChatProvider.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
11
import Foundation
2+
import SwiftUI
23

3-
public final class ChatProvider: ObservableObject, Equatable {
4+
public final class ChatProvider: ObservableObject {
45
@Published public var history: [ChatMessage] = []
56
@Published public var isReceivingMessage = false
67
public var onMessageSend: (String) -> Void
78
public var onStop: () -> Void
89
public var onClear: () -> Void
910
public var onClose: () -> Void
11+
public var onSwitchContext: () -> Void
1012

1113
public init(
1214
history: [ChatMessage] = [],
1315
isReceivingMessage: Bool = false,
1416
onMessageSend: @escaping (String) -> Void = { _ in },
1517
onStop: @escaping () -> Void = {},
1618
onClear: @escaping () -> Void = {},
17-
onClose: @escaping () -> Void = {}
19+
onClose: @escaping () -> Void = {},
20+
onSwitchContext: @escaping () -> Void = {}
1821
) {
1922
self.history = history
2023
self.isReceivingMessage = isReceivingMessage
2124
self.onMessageSend = onMessageSend
2225
self.onStop = onStop
2326
self.onClear = onClear
2427
self.onClose = onClose
28+
self.onSwitchContext = onSwitchContext
2529
}
2630

27-
public static func == (lhs: ChatProvider, rhs: ChatProvider) -> Bool {
28-
lhs.history == rhs.history && lhs.isReceivingMessage == rhs.isReceivingMessage
29-
}
30-
3131
public func send(_ message: String) { onMessageSend(message) }
3232
public func stop() { onStop() }
3333
public func clear() { onClear() }
3434
public func close() { onClose() }
35+
public func switchContext() { onSwitchContext() }
3536
}
3637

3738
public struct ChatMessage: Equatable {

0 commit comments

Comments
 (0)