Skip to content

Commit 6a24743

Browse files
committed
Merge branch 'feature/persisted-chat' into develop
2 parents 106417c + 98b464f commit 6a24743

17 files changed

Lines changed: 666 additions & 273 deletions

Core/Sources/ChatGPTChatTab/ChatContextMenu.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import SharedUIComponents
33
import SwiftUI
44

55
struct ChatContextMenu: View {
6-
let chat: ChatProvider
6+
@ObservedObject var chat: ChatProvider
77
@AppStorage(\.customCommands) var customCommands
88

99
var body: some View {

Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import ChatService
22
import ChatTab
33
import Combine
4+
import ComposableArchitecture
45
import Foundation
6+
import OpenAIService
57
import Preferences
68
import SwiftUI
79

@@ -13,18 +15,24 @@ public class ChatGPTChatTab: ChatTab {
1315
public let provider: ChatProvider
1416
private var cancellable = Set<AnyCancellable>()
1517

18+
struct RestorableState: Codable {
19+
var history: [OpenAIService.ChatMessage]
20+
var configuration: OverridingChatGPTConfiguration.Overriding
21+
var systemPrompt: String
22+
var extraSystemPrompt: String
23+
}
24+
1625
struct Builder: ChatTabBuilder {
1726
var title: String
18-
var buildable: Bool { true }
1927
var customCommand: CustomCommand?
28+
var afterBuild: (ChatGPTChatTab) async -> Void = { _ in }
2029

21-
func build() -> any ChatTab {
22-
let tab = ChatGPTChatTab()
23-
Task {
24-
if let customCommand {
25-
try await tab.service.handleCustomCommand(customCommand)
26-
}
30+
func build(store: StoreOf<ChatTabItem>) async -> (any ChatTab)? {
31+
let tab = ChatGPTChatTab(store: store)
32+
if let customCommand {
33+
try? await tab.service.handleCustomCommand(customCommand)
2734
}
35+
await afterBuild(tab)
2836
return tab
2937
}
3038
}
@@ -37,6 +45,32 @@ public class ChatGPTChatTab: ChatTab {
3745
ChatContextMenu(chat: provider)
3846
}
3947

48+
public func restorableState() async -> Data {
49+
let state = RestorableState(
50+
history: await service.memory.history,
51+
configuration: service.configuration.overriding,
52+
systemPrompt: service.systemPrompt,
53+
extraSystemPrompt: service.extraSystemPrompt
54+
)
55+
return (try? JSONEncoder().encode(state)) ?? Data()
56+
}
57+
58+
public static func restore(
59+
from data: Data,
60+
externalDependency: Void
61+
) async throws -> any ChatTabBuilder {
62+
let state = try JSONDecoder().decode(RestorableState.self, from: data)
63+
let builder = Builder(title: "Chat") { @MainActor tab in
64+
tab.service.configuration.overriding = state.configuration
65+
tab.service.mutateSystemPrompt(state.systemPrompt)
66+
tab.service.mutateExtraSystemPrompt(state.extraSystemPrompt)
67+
await tab.service.memory.mutateHistory { history in
68+
history = state.history
69+
}
70+
}
71+
return builder
72+
}
73+
4074
public static func chatBuilders(externalDependency: Void) -> [ChatTabBuilder] {
4175
let customCommands = UserDefaults.shared.value(for: \.customCommands).compactMap {
4276
command in
@@ -49,14 +83,33 @@ public class ChatGPTChatTab: ChatTab {
4983
return [Builder(title: "New Chat", customCommand: nil)] + customCommands
5084
}
5185

52-
public init(service: ChatService = .init()) {
86+
public init(service: ChatService = .init(), store: StoreOf<ChatTabItem>) {
5387
self.service = service
5488
provider = .init(service: service)
55-
super.init(id: "Chat-" + provider.id.uuidString, title: "Chat")
89+
super.init(store: store)
90+
}
5691

92+
public func start() {
93+
chatTabViewStore.send(.updateTitle("Chat"))
94+
95+
service.$systemPrompt.removeDuplicates().sink { _ in
96+
Task { @MainActor [weak self] in
97+
self?.chatTabViewStore.send(.tabContentUpdated)
98+
}
99+
}.store(in: &cancellable)
100+
101+
service.$extraSystemPrompt.removeDuplicates().sink { _ in
102+
Task { @MainActor [weak self] in
103+
self?.chatTabViewStore.send(.tabContentUpdated)
104+
}
105+
}.store(in: &cancellable)
106+
57107
provider.$history.sink { [weak self] _ in
58-
if let title = self?.provider.title {
59-
self?.title = title
108+
Task { @MainActor [weak self] in
109+
if let title = self?.provider.title {
110+
self?.chatTabViewStore.send(.updateTitle(title))
111+
}
112+
self?.chatTabViewStore.send(.tabContentUpdated)
60113
}
61114
}.store(in: &cancellable)
62115
}

Core/Sources/ChatService/ChatService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Preferences
77

88
public final class ChatService: ObservableObject {
99
public let memory: ContextAwareAutoManagedChatGPTMemory
10-
public let configuration: ChatGPTConfiguration
10+
public let configuration: OverridingChatGPTConfiguration
1111
public let chatGPTService: any ChatGPTServiceType
1212
public var allPluginCommands: [String] { allPlugins.map { $0.command } }
1313
@Published public internal(set) var isReceivingMessage = false
@@ -20,7 +20,7 @@ public final class ChatService: ObservableObject {
2020

2121
init<T: ChatGPTServiceType>(
2222
memory: ContextAwareAutoManagedChatGPTMemory,
23-
configuration: ChatGPTConfiguration,
23+
configuration: OverridingChatGPTConfiguration,
2424
chatGPTService: T
2525
) {
2626
self.memory = memory

Core/Sources/Service/GUI/ChatTabFactory.swift

Lines changed: 74 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import XcodeInspector
1111
import ProChatTabs
1212

1313
enum ChatTabFactory {
14-
static func chatTabBuilderCollection(
15-
openTab: @escaping (any ChatTab) -> Void
16-
) -> [ChatTabBuilderCollection] {
14+
static func chatTabBuilderCollection() -> [ChatTabBuilderCollection] {
1715
func folderIfNeeded(
1816
_ builders: [any ChatTabBuilder],
1917
title: String
@@ -27,82 +25,88 @@ enum ChatTabFactory {
2725

2826
let collection = [
2927
folderIfNeeded(ChatGPTChatTab.chatBuilders(), title: ChatGPTChatTab.name),
30-
folderIfNeeded(BrowserChatTab.chatBuilders(externalDependency: .init(
31-
getEditorContent: {
32-
guard let editor = XcodeInspector.shared.focusedEditor else {
33-
return .init(selectedText: "", language: "", fileContent: "")
34-
}
35-
let content = editor.content
36-
return .init(
37-
selectedText: content.selectedContent,
38-
language: languageIdentifierFromFileURL(
39-
XcodeInspector.shared
40-
.activeDocumentURL
41-
)
42-
.rawValue,
43-
fileContent: content.content
44-
)
45-
},
46-
handleCustomCommand: { command, prompt in
47-
switch command.feature {
48-
case let .chatWithSelection(extraSystemPrompt, _, useExtraSystemPrompt):
49-
let service = ChatService()
50-
return try await service.processMessage(
51-
systemPrompt: nil,
52-
extraSystemPrompt: (useExtraSystemPrompt ?? false) ? extraSystemPrompt :
53-
nil,
54-
prompt: prompt
55-
)
56-
case let .customChat(systemPrompt, _):
57-
let service = ChatService()
58-
return try await service.processMessage(
59-
systemPrompt: systemPrompt,
60-
extraSystemPrompt: nil,
61-
prompt: prompt
62-
)
63-
case let .singleRoundDialog(
64-
systemPrompt,
65-
overwriteSystemPrompt,
66-
_,
67-
_
68-
):
69-
let service = ChatService()
70-
return try await service.handleSingleRoundDialogCommand(
71-
systemPrompt: systemPrompt,
72-
overwriteSystemPrompt: overwriteSystemPrompt ?? false,
73-
prompt: prompt
74-
)
75-
case let .promptToCode(extraSystemPrompt, instruction, _, _):
76-
let service = PromptToCodeService(
77-
code: prompt,
78-
selectionRange: .outOfScope,
79-
language: .plaintext,
80-
identSize: 4,
81-
usesTabsForIndentation: true,
82-
projectRootURL: .init(fileURLWithPath: "/"),
83-
fileURL: .init(fileURLWithPath: "/"),
84-
allCode: prompt,
85-
extraSystemPrompt: extraSystemPrompt,
86-
generateDescriptionRequirement: false
87-
)
88-
try await service.modifyCode(prompt: instruction ?? "Modify content.")
89-
return service.code
90-
}
91-
},
92-
handleNewTab: openTab
93-
)), title: BrowserChatTab.name),
28+
folderIfNeeded(
29+
BrowserChatTab.chatBuilders(
30+
externalDependency: externalDependenciesForBrowserChatTab()
31+
),
32+
title: BrowserChatTab.name
33+
),
9434
].compactMap { $0 }
9535

9636
return collection
9737
}
38+
39+
static func externalDependenciesForBrowserChatTab() -> BrowserChatTab.ExternalDependency {
40+
.init(
41+
getEditorContent: {
42+
guard let editor = XcodeInspector.shared.focusedEditor else {
43+
return .init(selectedText: "", language: "", fileContent: "")
44+
}
45+
let content = editor.content
46+
return .init(
47+
selectedText: content.selectedContent,
48+
language: languageIdentifierFromFileURL(
49+
XcodeInspector.shared
50+
.activeDocumentURL
51+
)
52+
.rawValue,
53+
fileContent: content.content
54+
)
55+
},
56+
handleCustomCommand: { command, prompt in
57+
switch command.feature {
58+
case let .chatWithSelection(extraSystemPrompt, _, useExtraSystemPrompt):
59+
let service = ChatService()
60+
return try await service.processMessage(
61+
systemPrompt: nil,
62+
extraSystemPrompt: (useExtraSystemPrompt ?? false) ? extraSystemPrompt :
63+
nil,
64+
prompt: prompt
65+
)
66+
case let .customChat(systemPrompt, _):
67+
let service = ChatService()
68+
return try await service.processMessage(
69+
systemPrompt: systemPrompt,
70+
extraSystemPrompt: nil,
71+
prompt: prompt
72+
)
73+
case let .singleRoundDialog(
74+
systemPrompt,
75+
overwriteSystemPrompt,
76+
_,
77+
_
78+
):
79+
let service = ChatService()
80+
return try await service.handleSingleRoundDialogCommand(
81+
systemPrompt: systemPrompt,
82+
overwriteSystemPrompt: overwriteSystemPrompt ?? false,
83+
prompt: prompt
84+
)
85+
case let .promptToCode(extraSystemPrompt, instruction, _, _):
86+
let service = PromptToCodeService(
87+
code: prompt,
88+
selectionRange: .outOfScope,
89+
language: .plaintext,
90+
identSize: 4,
91+
usesTabsForIndentation: true,
92+
projectRootURL: .init(fileURLWithPath: "/"),
93+
fileURL: .init(fileURLWithPath: "/"),
94+
allCode: prompt,
95+
extraSystemPrompt: extraSystemPrompt,
96+
generateDescriptionRequirement: false
97+
)
98+
try await service.modifyCode(prompt: instruction ?? "Modify content.")
99+
return service.code
100+
}
101+
}
102+
)
103+
}
98104
}
99105

100106
#else
101107

102108
enum ChatTabFactory {
103-
static func chatTabBuilderCollection(
104-
openTab: @escaping (any ChatTab) -> Void
105-
) -> [ChatTabBuilderCollection] {
109+
static func chatTabBuilderCollection() -> [ChatTabBuilderCollection] {
106110
func folderIfNeeded(
107111
_ builders: [any ChatTabBuilder],
108112
title: String

0 commit comments

Comments
 (0)