Skip to content

Commit 2398a9c

Browse files
committed
Update modification panel to show all modifications
1 parent e8df300 commit 2398a9c

File tree

10 files changed

+221
-50
lines changed

10 files changed

+221
-50
lines changed

Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ struct GUI {
103103
chatTabPool.removeTab(of: id)
104104
}
105105

106-
case let .chatTab(_, .openNewTab(builder)):
106+
case let .chatTab(.element(_, .openNewTab(builder))):
107107
return .run { send in
108108
if let (_, chatTabInfo) = await chatTabPool
109109
.createTab(from: builder.chatTabBuilder)
@@ -223,7 +223,7 @@ struct GUI {
223223
await send(.suggestionWidget(.circularWidget(.widgetClicked)))
224224
}
225225

226-
case let .suggestionWidget(.chatPanel(.chatTab(id, .tabContentUpdated))):
226+
case let .suggestionWidget(.chatPanel(.chatTab(.element(id, .tabContentUpdated)))):
227227
#if canImport(ChatTabPersistent)
228228
// when a tab is updated, persist it.
229229
return .run { send in
@@ -319,7 +319,10 @@ public final class GraphicalUserInterfaceController {
319319
state.chatTabGroup.tabInfo[id: id] ?? .init(id: id, title: "")
320320
},
321321
action: { childAction in
322-
.suggestionWidget(.chatPanel(.chatTab(id: id, action: childAction)))
322+
.suggestionWidget(.chatPanel(.chatTab(.element(
323+
id: id,
324+
action: childAction
325+
))))
323326
}
324327
)
325328
}

Core/Sources/SuggestionWidget/ChatWindowView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ struct ChatTitleBar: View {
125125
}
126126
}
127127

128-
private extension View {
128+
extension View {
129129
func hideScrollIndicator() -> some View {
130130
if #available(macOS 13.0, *) {
131131
return scrollIndicators(.hidden)
@@ -200,7 +200,7 @@ struct ChatTabBar: View {
200200
draggingTabId: $draggingTabId
201201
)
202202
)
203-
203+
204204
} else {
205205
ChatTabBarButton(
206206
store: store,

Core/Sources/SuggestionWidget/FeatureReducers/ChatPanel.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public struct ChatPanel {
7777
case moveChatTab(from: Int, to: Int)
7878
case focusActiveChatTab
7979

80-
case chatTab(id: String, action: ChatTabItem.Action)
80+
case chatTab(IdentifiedActionOf<ChatTabItem>)
8181
}
8282

8383
@Dependency(\.chatTabPool) var chatTabPool
@@ -280,18 +280,18 @@ public struct ChatPanel {
280280
let id = state.chatTabGroup.selectedTabInfo?.id
281281
guard let id else { return .none }
282282
return .run { send in
283-
await send(.chatTab(id: id, action: .focus))
283+
await send(.chatTab(.element(id: id, action: .focus)))
284284
}
285285

286-
case let .chatTab(id, .close):
286+
case let .chatTab(.element(id, .close)):
287287
return .run { send in
288288
await send(.closeTabButtonClicked(id: id))
289289
}
290290

291291
case .chatTab:
292292
return .none
293293
}
294-
}.forEach(\.chatTabGroup.tabInfo, action: /Action.chatTab) {
294+
}.forEach(\.chatTabGroup.tabInfo, action: \.chatTab) {
295295
ChatTabItem()
296296
}
297297
}

Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,14 @@ public struct PromptToCodeGroup {
1111
public var promptToCodes: IdentifiedArrayOf<PromptToCodePanel.State> = []
1212
public var activeDocumentURL: PromptToCodePanel.State.ID? = XcodeInspector.shared
1313
.realtimeActiveDocumentURL
14+
public var selectedTabId: URL?
1415
public var activePromptToCode: PromptToCodePanel.State? {
1516
get {
16-
if let detached = promptToCodes
17-
.first(where: { !$0.promptToCodeState.isAttachedToTarget })
18-
{
19-
return detached
20-
}
21-
guard let id = activeDocumentURL else { return nil }
22-
return promptToCodes[id: id]
17+
guard let selectedTabId else { return promptToCodes.first }
18+
return promptToCodes[id: selectedTabId] ?? promptToCodes.first
2319
}
2420
set {
25-
if let id = newValue?.id {
26-
promptToCodes[id: id] = newValue
27-
}
21+
selectedTabId = newValue?.id
2822
}
2923
}
3024
}
@@ -41,7 +35,11 @@ public struct PromptToCodeGroup {
4135
case discardAcceptedPromptToCodeIfNotContinuous(id: PromptToCodePanel.State.ID)
4236
case updateActivePromptToCode(documentURL: URL)
4337
case discardExpiredPromptToCode(documentURLs: [URL])
44-
case promptToCode(PromptToCodePanel.State.ID, PromptToCodePanel.Action)
38+
case tabClicked(id: URL)
39+
case closeTabButtonClicked(id: URL)
40+
case switchToNextTab
41+
case switchToPreviousTab
42+
case promptToCode(IdentifiedActionOf<PromptToCodePanel>)
4543
case activePromptToCode(PromptToCodePanel.Action)
4644
}
4745

@@ -51,22 +49,28 @@ public struct PromptToCodeGroup {
5149
Reduce { state, action in
5250
switch action {
5351
case let .activateOrCreatePromptToCode(s):
54-
if let promptToCode = state.activePromptToCode {
52+
if let promptToCode = state.activePromptToCode, s.id == promptToCode.id {
5553
return .run { send in
56-
await send(.promptToCode(promptToCode.id, .focusOnTextField))
54+
await send(.promptToCode(.element(
55+
id: promptToCode.id,
56+
action: .focusOnTextField
57+
)))
5758
}
5859
}
5960
return .run { send in
6061
await send(.createPromptToCode(s, sendImmediately: false))
6162
}
6263
case let .createPromptToCode(newPromptToCode, sendImmediately):
6364
// insert at 0 so it has high priority then the other detached prompt to codes
64-
state.promptToCodes.insert(newPromptToCode, at: 0)
65+
state.promptToCodes.append(newPromptToCode)
6566
return .run { send in
6667
if sendImmediately,
6768
!newPromptToCode.contextInputController.instruction.string.isEmpty
6869
{
69-
await send(.promptToCode(newPromptToCode.id, .modifyCodeButtonTapped))
70+
await send(.promptToCode(.element(
71+
id: newPromptToCode.id,
72+
action: .modifyCodeButtonTapped
73+
)))
7074
}
7175
}.cancellable(
7276
id: PromptToCodePanel.CancellationKey.modifyCode(newPromptToCode.id),
@@ -94,6 +98,37 @@ public struct PromptToCodeGroup {
9498
}
9599
return .none
96100

101+
case let .tabClicked(id):
102+
state.selectedTabId = id
103+
return .none
104+
105+
case let .closeTabButtonClicked(id):
106+
return .run { send in
107+
await send(.promptToCode(.element(
108+
id: id,
109+
action: .cancelButtonTapped
110+
)))
111+
}
112+
113+
case .switchToNextTab:
114+
if let selectedTabId = state.selectedTabId,
115+
let index = state.promptToCodes.index(id: selectedTabId)
116+
{
117+
let nextIndex = (index + 1) % state.promptToCodes.count
118+
state.selectedTabId = state.promptToCodes[nextIndex].id
119+
}
120+
return .none
121+
122+
case .switchToPreviousTab:
123+
if let selectedTabId = state.selectedTabId,
124+
let index = state.promptToCodes.index(id: selectedTabId)
125+
{
126+
let previousIndex = (index - 1 + state.promptToCodes.count) % state
127+
.promptToCodes.count
128+
state.selectedTabId = state.promptToCodes[previousIndex].id
129+
}
130+
return .none
131+
97132
case .promptToCode:
98133
return .none
99134

@@ -104,22 +139,28 @@ public struct PromptToCodeGroup {
104139
.ifLet(\.activePromptToCode, action: \.activePromptToCode) {
105140
PromptToCodePanel()
106141
}
107-
.forEach(\.promptToCodes, action: /Action.promptToCode, element: {
142+
.forEach(\.promptToCodes, action: \.promptToCode, element: {
108143
PromptToCodePanel()
109144
})
110145

111146
Reduce { state, action in
112147
switch action {
113-
case let .promptToCode(id, .cancelButtonTapped):
148+
case let .promptToCode(.element(id, .cancelButtonTapped)):
114149
state.promptToCodes.remove(id: id)
150+
let isEmpty = state.promptToCodes.isEmpty
115151
return .run { _ in
116-
activatePreviousActiveXcode()
152+
if isEmpty {
153+
activatePreviousActiveXcode()
154+
}
117155
}
118156
case .activePromptToCode(.cancelButtonTapped):
119-
guard let id = state.activePromptToCode?.id else { return .none }
157+
guard let id = state.selectedTabId else { return .none }
120158
state.promptToCodes.remove(id: id)
159+
let isEmpty = state.promptToCodes.isEmpty
121160
return .run { _ in
122-
activatePreviousActiveXcode()
161+
if isEmpty {
162+
activatePreviousActiveXcode()
163+
}
123164
}
124165
default: return .none
125166
}

Core/Sources/SuggestionWidget/FeatureReducers/SharedPanel.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ public struct SharedPanel {
77
public struct Content {
88
public var promptToCodeGroup = PromptToCodeGroup.State()
99
var suggestion: PresentingCodeSuggestion?
10-
public var promptToCode: PromptToCodePanel.State? { promptToCodeGroup.activePromptToCode }
1110
var error: String?
1211
}
1312

@@ -19,7 +18,7 @@ public struct SharedPanel {
1918
var isPanelDisplayed: Bool = false
2019
var isEmpty: Bool {
2120
if content.error != nil { return false }
22-
if content.promptToCode != nil { return false }
21+
if !content.promptToCodeGroup.promptToCodes.isEmpty { return false }
2322
if content.suggestion != nil,
2423
UserDefaults.shared
2524
.value(for: \.suggestionPresentationMode) == .floatingWidget { return false }

Core/Sources/SuggestionWidget/FeatureReducers/WidgetPanel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public struct WidgetPanel {
118118

119119
case .sharedPanel(.promptToCodeGroup(.activateOrCreatePromptToCode)),
120120
.sharedPanel(.promptToCodeGroup(.createPromptToCode)):
121-
let hasPromptToCode = state.content.promptToCode != nil
121+
let hasPromptToCode = !state.content.promptToCodeGroup.promptToCodes.isEmpty
122122
return .run { send in
123123
await send(.displayPanelContent)
124124

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import ComposableArchitecture
2+
import Foundation
3+
import SwiftUI
4+
5+
struct PromptToCodePanelGroupView: View {
6+
let store: StoreOf<PromptToCodeGroup>
7+
8+
var body: some View {
9+
WithPerceptionTracking {
10+
VStack(spacing: 0) {
11+
PromptToCodeTabBar(store: store)
12+
.frame(height: 26)
13+
14+
Divider()
15+
16+
if let store = self.store.scope(
17+
state: \.activePromptToCode,
18+
action: \.activePromptToCode
19+
) {
20+
PromptToCodePanelView(store: store)
21+
}
22+
}
23+
.background(.ultraThickMaterial)
24+
.xcodeStyleFrame()
25+
}
26+
}
27+
}
28+
29+
struct PromptToCodeTabBar: View {
30+
let store: StoreOf<PromptToCodeGroup>
31+
32+
struct TabInfo: Equatable, Identifiable {
33+
var id: URL
34+
var tabTitle: String
35+
var isProcessing: Bool
36+
}
37+
38+
var body: some View {
39+
HStack(spacing: 0) {
40+
Divider()
41+
Tabs(store: store)
42+
}
43+
.background {
44+
Button(action: { store.send(.switchToNextTab) }) { EmptyView() }
45+
.opacity(0)
46+
.keyboardShortcut("]", modifiers: [.command, .shift])
47+
Button(action: { store.send(.switchToPreviousTab) }) { EmptyView() }
48+
.opacity(0)
49+
.keyboardShortcut("[", modifiers: [.command, .shift])
50+
}
51+
}
52+
53+
struct Tabs: View {
54+
let store: StoreOf<PromptToCodeGroup>
55+
56+
var body: some View {
57+
WithPerceptionTracking {
58+
let tabInfo = store.promptToCodes.map {
59+
TabInfo(
60+
id: $0.id,
61+
tabTitle: $0.filename,
62+
isProcessing: $0.promptToCodeState.isGenerating
63+
)
64+
}
65+
let selectedTabId = store.selectedTabId
66+
?? store.promptToCodes.first?.id
67+
68+
ScrollViewReader { proxy in
69+
ScrollView(.horizontal) {
70+
HStack(spacing: 0) {
71+
ForEach(tabInfo) { info in
72+
WithPerceptionTracking {
73+
PromptToCodeTabBarButton(
74+
store: store,
75+
info: info,
76+
isSelected: info.id == store.selectedTabId
77+
)
78+
.id(info.id)
79+
}
80+
}
81+
}
82+
}
83+
.hideScrollIndicator()
84+
.onChange(of: selectedTabId) { id in
85+
withAnimation(.easeInOut(duration: 0.2)) {
86+
proxy.scrollTo(id)
87+
}
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
struct PromptToCodeTabBarButton: View {
96+
let store: StoreOf<PromptToCodeGroup>
97+
let info: PromptToCodeTabBar.TabInfo
98+
let isSelected: Bool
99+
@State var isHovered: Bool = false
100+
101+
var body: some View {
102+
HStack(spacing: 0) {
103+
HStack(spacing: 4) {
104+
if info.isProcessing {
105+
ProgressView()
106+
.controlSize(.small)
107+
}
108+
Text(info.tabTitle)
109+
}
110+
.font(.callout)
111+
.lineLimit(1)
112+
.frame(maxWidth: 120)
113+
.padding(.horizontal, 28)
114+
.contentShape(Rectangle())
115+
.onTapGesture {
116+
store.send(.tabClicked(id: info.id))
117+
}
118+
.overlay(alignment: .leading) {
119+
Button(action: {
120+
store.send(.closeTabButtonClicked(id: info.id))
121+
}) {
122+
Image(systemName: "xmark")
123+
.foregroundColor(.secondary)
124+
}
125+
.buttonStyle(.plain)
126+
.padding(2)
127+
.padding(.leading, 8)
128+
.opacity(isHovered ? 1 : 0)
129+
}
130+
.onHover { isHovered = $0 }
131+
.animation(.linear(duration: 0.1), value: isHovered)
132+
.animation(.linear(duration: 0.1), value: isSelected)
133+
134+
Divider().padding(.vertical, 6)
135+
}
136+
.background(isSelected ? Color(nsColor: .selectedControlColor) : Color.clear)
137+
.frame(maxHeight: .infinity)
138+
}
139+
}
140+

0 commit comments

Comments
 (0)