Skip to content

Commit ac3f944

Browse files
committed
Migrate chat tab to a more maintainable structure
1 parent ba53c27 commit ac3f944

File tree

12 files changed

+373
-179
lines changed

12 files changed

+373
-179
lines changed

Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ChatService
22
import ChatTab
33
import Combine
4+
import ComposableArchitecture
45
import Foundation
56
import OpenAIService
67
import Preferences
@@ -24,8 +25,8 @@ public class ChatGPTChatTab: ChatTab {
2425
var buildable: Bool { true }
2526
var customCommand: CustomCommand?
2627

27-
func build() -> any ChatTab {
28-
let tab = ChatGPTChatTab()
28+
func build(store: StoreOf<ChatTabItem>) -> any ChatTab {
29+
let tab = ChatGPTChatTab(store: store)
2930
Task {
3031
if let customCommand {
3132
try await tab.service.handleCustomCommand(customCommand)
@@ -53,10 +54,11 @@ public class ChatGPTChatTab: ChatTab {
5354

5455
public static func restore(
5556
from data: Data,
57+
store: StoreOf<ChatTabItem>,
5658
externalDependency: Void
5759
) async throws -> any ChatTab {
5860
let state = try JSONDecoder().decode(RestorableState.self, from: data)
59-
let tab = ChatGPTChatTab()
61+
let tab = ChatGPTChatTab(store: store)
6062
tab.service.configuration.overriding = state.configuration
6163
await tab.service.memory.mutateHistory { history in
6264
history = state.history
@@ -77,14 +79,19 @@ public class ChatGPTChatTab: ChatTab {
7779
return [Builder(title: "New Chat", customCommand: nil)] + customCommands
7880
}
7981

80-
public init(service: ChatService = .init()) {
82+
public init(service: ChatService = .init(), store: StoreOf<ChatTabItem>) {
8183
self.service = service
8284
provider = .init(service: service)
83-
super.init(id: "Chat-" + provider.id.uuidString, title: "Chat")
84-
85+
super.init(store: store)
86+
}
87+
88+
public func start() {
89+
chatTabViewStore.send(.updateTitle("Chat"))
8590
provider.$history.sink { [weak self] _ in
86-
if let title = self?.provider.title {
87-
self?.title = title
91+
Task { @MainActor [weak self] in
92+
if let title = self?.provider.title {
93+
self?.chatTabViewStore.send(.updateTitle(title))
94+
}
8895
}
8996
}.store(in: &cancellable)
9097
}

Core/Sources/Service/GUI/ChatTabFactory.swift

Lines changed: 3 additions & 8 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
@@ -88,8 +86,7 @@ enum ChatTabFactory {
8886
try await service.modifyCode(prompt: instruction ?? "Modify content.")
8987
return service.code
9088
}
91-
},
92-
handleNewTab: openTab
89+
}
9390
)), title: BrowserChatTab.name),
9491
].compactMap { $0 }
9592

@@ -100,9 +97,7 @@ enum ChatTabFactory {
10097
#else
10198

10299
enum ChatTabFactory {
103-
static func chatTabBuilderCollection(
104-
openTab: @escaping (any ChatTab) -> Void
105-
) -> [ChatTabBuilderCollection] {
100+
static func chatTabBuilderCollection() -> [ChatTabBuilderCollection] {
106101
func folderIfNeeded(
107102
_ builders: [any ChatTabBuilder],
108103
title: String

Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ struct GUI: ReducerProtocol {
2525
case suggestionWidget(WidgetFeature.Action)
2626
}
2727

28+
@Dependency(\.chatTabPool) var chatTabPool: ChatTabPool
29+
2830
var body: some ReducerProtocol<State, Action> {
2931
Scope(state: \.suggestionWidgetState, action: /Action.suggestionWidget) {
3032
WidgetFeature()
@@ -37,16 +39,24 @@ struct GUI: ReducerProtocol {
3739
Reduce { _, action in
3840
switch action {
3941
case let .createNewTapButtonClicked(kind):
40-
guard let builder = kind?.builder else {
41-
let chatTap = ChatGPTChatTab()
42-
return .run { send in
43-
await send(.appendAndSelectTab(chatTap))
42+
return .run { send in
43+
if let (_, chatTabInfo) = await chatTabPool.createTab(for: kind) {
44+
await send(.appendAndSelectTab(chatTabInfo))
4445
}
4546
}
46-
guard builder.buildable else { return .none }
47-
let chatTap = builder.build()
47+
48+
case let .closeTabButtonClicked(id):
49+
return .run { _ in
50+
chatTabPool.removeTab(of: id)
51+
}
52+
53+
case let .chatTab(_, .openNewTab(builder)):
4854
return .run { send in
49-
await send(.appendAndSelectTab(chatTap))
55+
if let (_, chatTabInfo) = await chatTabPool
56+
.createTab(from: builder.chatTabBuilder)
57+
{
58+
await send(.appendAndSelectTab(chatTabInfo))
59+
}
5060
}
5161

5262
default:
@@ -65,12 +75,16 @@ struct GUI: ReducerProtocol {
6575
}
6676

6777
case .createChatGPTChatTabIfNeeded:
68-
if state.chatTabGroup.tabs.contains(where: { $0 is ChatGPTChatTab }) {
78+
if state.chatTabGroup.tabInfo.contains(where: {
79+
chatTabPool.getTab(of: $0.id) is ChatGPTChatTab
80+
}) {
6981
return .none
7082
}
71-
let chatTab = ChatGPTChatTab()
72-
state.chatTabGroup.tabs.append(chatTab)
73-
return .none
83+
return .run { send in
84+
if let (_, chatTabInfo) = await chatTabPool.createTab(for: nil) {
85+
await send(.suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo))))
86+
}
87+
}
7488

7589
case let .sendCustomCommandToActiveChat(command):
7690
@Sendable func stopAndHandleCommand(_ tab: ChatGPTChatTab) async {
@@ -80,28 +94,36 @@ struct GUI: ReducerProtocol {
8094
try? await tab.service.handleCustomCommand(command)
8195
}
8296

83-
if let activeTab = state.chatTabGroup.activeChatTab as? ChatGPTChatTab {
97+
if let info = state.chatTabGroup.selectedTabInfo,
98+
let activeTab = chatTabPool.getTab(of: info.id) as? ChatGPTChatTab
99+
{
84100
return .run { send in
85101
await send(.openChatPanel(forceDetach: false))
86102
await stopAndHandleCommand(activeTab)
87103
}
88104
}
89105

90-
if let chatTab = state.chatTabGroup.tabs.first(where: {
91-
guard $0 is ChatGPTChatTab else { return false }
92-
return true
93-
}) as? ChatGPTChatTab {
106+
if let info = state.chatTabGroup.tabInfo.first(where: {
107+
chatTabPool.getTab(of: $0.id) is ChatGPTChatTab
108+
}),
109+
let chatTab = chatTabPool.getTab(of: info.id) as? ChatGPTChatTab
110+
{
94111
state.chatTabGroup.selectedTabId = chatTab.id
95112
return .run { send in
96113
await send(.openChatPanel(forceDetach: false))
97114
await stopAndHandleCommand(chatTab)
98115
}
99116
}
100-
let chatTab = ChatGPTChatTab()
101-
state.chatTabGroup.tabs.append(chatTab)
117+
102118
return .run { send in
119+
guard let (chatTab, chatTabInfo) = await chatTabPool.createTab(for: nil) else {
120+
return
121+
}
122+
await send(.suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo))))
103123
await send(.openChatPanel(forceDetach: false))
104-
await stopAndHandleCommand(chatTab)
124+
if let chatTab = chatTab as? ChatGPTChatTab {
125+
await stopAndHandleCommand(chatTab)
126+
}
105127
}
106128

107129
case .suggestionWidget:
@@ -118,31 +140,28 @@ public final class GraphicalUserInterfaceController {
118140
let widgetController: SuggestionWidgetController
119141
let widgetDataSource: WidgetDataSource
120142
let viewStore: ViewStoreOf<GUI>
143+
let chatTabPool: ChatTabPool
121144

122145
class WeakStoreHolder {
123146
weak var store: StoreOf<GUI>?
124147
}
125148

126149
private init() {
127-
let weakStoreHolder = WeakStoreHolder()
150+
let chatTabPool = ChatTabPool()
128151
let suggestionDependency = SuggestionWidgetControllerDependency()
129152
let setupDependency: (inout DependencyValues) -> Void = { dependencies in
130153
dependencies.suggestionWidgetControllerDependency = suggestionDependency
131154
dependencies.suggestionWidgetUserDefaultsObservers = .init()
132-
dependencies.chatTabBuilderCollection = {
133-
ChatTabFactory.chatTabBuilderCollection { tab in
134-
weakStoreHolder.store?
135-
.send(.suggestionWidget(.chatPanel(.appendAndSelectTab(tab))))
136-
}
137-
}
155+
dependencies.chatTabPool = chatTabPool
156+
dependencies.chatTabBuilderCollection = ChatTabFactory.chatTabBuilderCollection
138157
}
139158
let store = StoreOf<GUI>(
140159
initialState: .init(),
141160
reducer: GUI(),
142161
prepareDependencies: setupDependency
143162
)
144-
weakStoreHolder.store = store
145163
self.store = store
164+
self.chatTabPool = chatTabPool
146165
viewStore = ViewStore(store)
147166
widgetDataSource = .init()
148167

@@ -151,9 +170,22 @@ public final class GraphicalUserInterfaceController {
151170
state: \.suggestionWidgetState,
152171
action: GUI.Action.suggestionWidget
153172
),
173+
chatTabPool: chatTabPool,
154174
dependency: suggestionDependency
155175
)
156176

177+
chatTabPool.createStore = { id in
178+
store.scope(
179+
state: { state in
180+
state.chatTabGroup.tabInfo[id: id]
181+
?? .init(id: id, title: "")
182+
},
183+
action: { childAction in
184+
.suggestionWidget(.chatPanel(.chatTab(id: id, action: childAction)))
185+
}
186+
)
187+
}
188+
157189
suggestionDependency.suggestionWidgetDataSource = widgetDataSource
158190
suggestionDependency.onOpenChatClicked = { [weak self] in
159191
Task { [weak self] in
@@ -177,3 +209,34 @@ public final class GraphicalUserInterfaceController {
177209
}
178210
}
179211

212+
extension ChatTabPool {
213+
@MainActor
214+
func createTab(
215+
from builder: ChatTabBuilder
216+
) -> (any ChatTab, ChatTabInfo)? {
217+
let id = UUID().uuidString
218+
let info = ChatTabInfo(id: id, title: "")
219+
guard builder.buildable else { return nil }
220+
let chatTap = builder.build(store: createStore(id))
221+
setTab(chatTap)
222+
return (chatTap, info)
223+
}
224+
225+
@MainActor
226+
func createTab(
227+
for kind: ChatTabKind?
228+
) -> (any ChatTab, ChatTabInfo)? {
229+
let id = UUID().uuidString
230+
let info = ChatTabInfo(id: id, title: "")
231+
guard let builder = kind?.builder else {
232+
let chatTap = ChatGPTChatTab(store: createStore(id))
233+
setTab(chatTap)
234+
return (chatTap, info)
235+
}
236+
guard builder.buildable else { return nil }
237+
let chatTap = builder.build(store: createStore(id))
238+
setTab(chatTap)
239+
return (chatTap, info)
240+
}
241+
}
242+

0 commit comments

Comments
 (0)