Skip to content

Commit bf87192

Browse files
committed
Merge branch 'feature/chat-panel-tweaks' into develop
2 parents 69859c5 + 7368c5e commit bf87192

8 files changed

Lines changed: 197 additions & 123 deletions

File tree

Config.debug.xcconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
SLASH = /
33

44
PRODUCT_NAME = Copilot for Xcode Dev
5+
HOST_APP_NAME = Copilot for Xcode Dev
56
BUNDLE_IDENTIFIER_BASE = dev.com.intii.CopilotForXcode
67
EXTENSION_BUNDLE_NAME = Copilot Dev
78
SPARKLE_FEED_URL = http:$(SLASH)$(SLASH)127.0.0.1:9433/appcast.xml

Core/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ public final class ActiveApplicationMonitor {
44
static let shared = ActiveApplicationMonitor()
55
var latestXcode: NSRunningApplication? = NSWorkspace.shared.runningApplications
66
.first(where: \.isXcode)
7+
var previousApp: NSRunningApplication?
78
var activeApplication = NSWorkspace.shared.runningApplications.first(where: \.isActive) {
89
didSet {
910
if activeApplication?.isXcode ?? false {
1011
latestXcode = activeApplication
1112
}
13+
previousApp = oldValue
1214
}
1315
}
1416

@@ -35,6 +37,8 @@ public final class ActiveApplicationMonitor {
3537
}
3638

3739
public static var activeApplication: NSRunningApplication? { shared.activeApplication }
40+
41+
public static var previousActiveApplication: NSRunningApplication? { shared.previousApp }
3842

3943
public static var activeXcode: NSRunningApplication? {
4044
if activeApplication?.isXcode ?? false {
@@ -77,3 +81,4 @@ public final class ActiveApplicationMonitor {
7781
public extension NSRunningApplication {
7882
var isXcode: Bool { bundleIdentifier == "com.apple.dt.Xcode" }
7983
}
84+

Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,15 @@ public final class GraphicalUserInterfaceController {
3333
}
3434
}
3535
}
36+
37+
public func openGlobalChat() {
38+
UserDefaults.shared.set(true, for: \.useGlobalChat)
39+
let dataSource = WidgetDataSource.shared
40+
let fakeFileURL = URL(fileURLWithPath: "/")
41+
Task {
42+
await dataSource.createChatIfNeeded(for: fakeFileURL)
43+
let presenter = PresentInWindowSuggestionPresenter()
44+
presenter.presentChatRoom(fileURL: fakeFileURL)
45+
}
46+
}
3647
}

Core/Sources/Service/GUI/WidgetDataSource.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import ActiveApplicationMonitor
12
import ChatService
2-
import SuggestionModel
3-
import GitHubCopilotService
43
import Foundation
4+
import GitHubCopilotService
55
import OpenAIService
66
import PromptToCodeService
7+
import SuggestionModel
78
import SuggestionWidget
89

910
@ServiceActor
@@ -52,6 +53,9 @@ final class WidgetDataSource {
5253
}
5354
let presenter = PresentInWindowSuggestionPresenter()
5455
presenter.closeChatRoom(fileURL: url)
56+
if let app = ActiveApplicationMonitor.previousActiveApplication, app.isXcode {
57+
app.activate()
58+
}
5559
},
5660
onSwitchContext: { [weak self] in
5761
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
@@ -114,6 +118,9 @@ final class WidgetDataSource {
114118
self?.removePromptToCode(for: url)
115119
let presenter = PresentInWindowSuggestionPresenter()
116120
presenter.closePromptToCode(fileURL: url)
121+
if let app = ActiveApplicationMonitor.previousActiveApplication, app.isXcode {
122+
app.activate()
123+
}
117124
}
118125
)
119126
return PromptToCode(promptToCodeService: service, provider: provider)
@@ -199,3 +206,4 @@ extension WidgetDataSource: SuggestionWidgetDataSource {
199206
return promptToCodes[url]?.provider
200207
}
201208
}
209+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import SwiftUI
2+
3+
struct CustomTextEditor: NSViewRepresentable {
4+
func makeCoordinator() -> Coordinator {
5+
Coordinator(self)
6+
}
7+
8+
@Binding var text: String
9+
let font: NSFont
10+
let onSubmit: () -> Void
11+
12+
func makeNSView(context: Context) -> NSScrollView {
13+
let textView = (context.coordinator.theTextView.documentView as! NSTextView)
14+
textView.delegate = context.coordinator
15+
textView.string = text
16+
textView.font = font
17+
textView.allowsUndo = true
18+
textView.drawsBackground = false
19+
20+
return context.coordinator.theTextView
21+
}
22+
23+
func updateNSView(_ nsView: NSScrollView, context: Context) {
24+
let textView = (context.coordinator.theTextView.documentView as! NSTextView)
25+
textView.string = text
26+
}
27+
}
28+
29+
extension CustomTextEditor {
30+
class Coordinator: NSObject, NSTextViewDelegate {
31+
var view: CustomTextEditor
32+
var theTextView = NSTextView.scrollableTextView()
33+
var affectedCharRange: NSRange?
34+
35+
init(_ view: CustomTextEditor) {
36+
self.view = view
37+
}
38+
39+
func textDidChange(_ notification: Notification) {
40+
guard let textView = notification.object as? NSTextView else {
41+
return
42+
}
43+
44+
view.text = textView.string
45+
}
46+
47+
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
48+
if commandSelector == #selector(NSTextView.insertNewline(_:)) {
49+
if let event = NSApplication.shared.currentEvent,
50+
!event.modifierFlags.contains(.shift),
51+
event.keyCode == 36
52+
{
53+
view.onSubmit()
54+
return true
55+
}
56+
}
57+
58+
return false
59+
}
60+
61+
func textView(
62+
_ textView: NSTextView,
63+
shouldChangeTextIn affectedCharRange: NSRange,
64+
replacementString: String?
65+
) -> Bool {
66+
return true
67+
}
68+
}
69+
}
70+

Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift

Lines changed: 85 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -250,80 +250,99 @@ struct ChatPanelInputArea: View {
250250

251251
var body: some View {
252252
HStack {
253-
// clear button
254-
Button(action: {
255-
chat.clear()
256-
}) {
257-
Group {
258-
if #available(macOS 13.0, *) {
259-
Image(systemName: "eraser.line.dashed.fill")
260-
} else {
261-
Image(systemName: "trash.fill")
262-
}
263-
}
264-
.padding(6)
265-
.background {
266-
Circle().fill(Color(nsColor: .controlBackgroundColor))
267-
}
268-
.overlay {
269-
Circle()
270-
.stroke(Color(nsColor: .controlColor), lineWidth: 1)
271-
}
272-
}
273-
.buttonStyle(.plain)
253+
clearButton
254+
textEditor
255+
}
256+
.onAppear {
257+
isInputAreaFocused = true
258+
}
259+
.padding(8)
260+
.background(.ultraThickMaterial)
261+
}
274262

275-
HStack(spacing: 0) {
276-
Group {
277-
if #available(macOS 13.0, *) {
278-
TextField("Type a message", text: $typedMessage, axis: .vertical)
279-
} else {
280-
TextEditor(text: $typedMessage)
281-
.frame(height: 42, alignment: .leading)
282-
.font(.body)
283-
.background(Color.clear)
284-
}
285-
}
286-
.focused($isInputAreaFocused)
287-
.lineLimit(3)
288-
.multilineTextAlignment(.leading)
289-
.textFieldStyle(.plain)
290-
.padding(8)
291-
292-
Button(action: {
293-
if typedMessage.isEmpty { return }
294-
chat.send(typedMessage)
295-
typedMessage = ""
296-
}) {
297-
Image(systemName: "paperplane.fill")
298-
.padding(8)
263+
var clearButton: some View {
264+
Button(action: {
265+
chat.clear()
266+
}) {
267+
Group {
268+
if #available(macOS 13.0, *) {
269+
Image(systemName: "eraser.line.dashed.fill")
270+
} else {
271+
Image(systemName: "trash.fill")
299272
}
300-
.buttonStyle(.plain)
301-
.disabled(chat.isReceivingMessage)
302-
.keyboardShortcut(KeyEquivalent.return, modifiers: [])
303273
}
304-
.frame(maxWidth: .infinity)
274+
.padding(6)
305275
.background {
306-
RoundedRectangle(cornerRadius: 6)
307-
.fill(Color(nsColor: .controlBackgroundColor))
276+
Circle().fill(Color(nsColor: .controlBackgroundColor))
308277
}
309278
.overlay {
310-
RoundedRectangle(cornerRadius: 6)
279+
Circle()
311280
.stroke(Color(nsColor: .controlColor), lineWidth: 1)
312281
}
313-
.background {
314-
Button(action: {
315-
typedMessage += "\n"
316-
}) {
317-
EmptyView()
318-
}
319-
.keyboardShortcut(KeyEquivalent.return, modifiers: [.shift])
282+
}
283+
.buttonStyle(.plain)
284+
}
285+
286+
var textEditor: some View {
287+
HStack(spacing: 0) {
288+
ZStack(alignment: .center) {
289+
// a hack to support dynamic height of TextEditor
290+
Text(typedMessage.isEmpty ? "Hi" : typedMessage).opacity(0)
291+
.font(.system(size: 14))
292+
.frame(maxWidth: .infinity, maxHeight: 400)
293+
.overlay(alignment: .leadingFirstTextBaseline) {
294+
Text("Write something..")
295+
.foregroundColor(.secondary)
296+
.font(.system(size: 14))
297+
.opacity(typedMessage.isEmpty ? 1 : 0)
298+
}
299+
.padding(.top, 0)
300+
.padding(.bottom, 3)
301+
.padding(.horizontal, 4)
302+
303+
CustomTextEditor(
304+
text: $typedMessage,
305+
font: .systemFont(ofSize: 14),
306+
onSubmit: { submitText() }
307+
)
320308
}
309+
.focused($isInputAreaFocused)
310+
.padding(8)
311+
.fixedSize(horizontal: false, vertical: true)
312+
313+
Button(action: {
314+
submitText()
315+
}) {
316+
Image(systemName: "paperplane.fill")
317+
.padding(8)
318+
}
319+
.buttonStyle(.plain)
320+
.disabled(chat.isReceivingMessage)
321+
.keyboardShortcut(KeyEquivalent.return, modifiers: [])
321322
}
322-
.onAppear {
323-
isInputAreaFocused = true
323+
.frame(maxWidth: .infinity)
324+
.background {
325+
RoundedRectangle(cornerRadius: 6)
326+
.fill(Color(nsColor: .controlBackgroundColor))
324327
}
325-
.padding(8)
326-
.background(.ultraThickMaterial)
328+
.overlay {
329+
RoundedRectangle(cornerRadius: 6)
330+
.stroke(Color(nsColor: .controlColor), lineWidth: 1)
331+
}
332+
.background {
333+
Button(action: {
334+
typedMessage += "\n"
335+
}) {
336+
EmptyView()
337+
}
338+
.keyboardShortcut(KeyEquivalent.return, modifiers: [.shift])
339+
}
340+
}
341+
342+
func submitText() {
343+
if typedMessage.isEmpty { return }
344+
chat.send(typedMessage)
345+
typedMessage = ""
327346
}
328347
}
329348

@@ -386,6 +405,9 @@ struct RoundedCorners: Shape {
386405
struct GlobalChatSwitchToggleStyle: ToggleStyle {
387406
func makeBody(configuration: Configuration) -> some View {
388407
HStack(spacing: 4) {
408+
Text(configuration.isOn ? "Shared Conversation" : "Local Conversation")
409+
.foregroundStyle(.tertiary)
410+
389411
RoundedRectangle(cornerRadius: 10, style: .circular)
390412
.foregroundColor(configuration.isOn ? Color.indigo : .gray.opacity(0.5))
391413
.frame(width: 30, height: 20, alignment: .center)
@@ -411,9 +433,6 @@ struct GlobalChatSwitchToggleStyle: ToggleStyle {
411433
RoundedRectangle(cornerRadius: 10, style: .circular)
412434
.stroke(.black.opacity(0.2), lineWidth: 1)
413435
}
414-
415-
Text(configuration.isOn ? "Global Chat" : "File Chat")
416-
.foregroundStyle(.tertiary)
417436
}
418437
}
419438
}

Core/Sources/SuggestionWidget/WidgetView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ struct WidgetContextMenu: View {
152152
Button(action: {
153153
useGlobalChat.toggle()
154154
}) {
155-
Text("Use Global Chat")
155+
Text("Use Shared Conversation")
156156
if useGlobalChat {
157157
Image(systemName: "checkmark")
158158
}

0 commit comments

Comments
 (0)