Skip to content

Commit 0ea2342

Browse files
committed
Merge branch 'release/0.10.0'
2 parents 6e57297 + 6f54032 commit 0ea2342

36 files changed

+940
-155
lines changed

Copilot for Xcode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Copilot for Xcode/OpenAIView.swift

Lines changed: 98 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Preferences
55
import SwiftUI
66

77
final class OpenAIViewSettings: ObservableObject {
8+
static let availableLocalizedLocales = Locale.availableLocalizedLocales
89
@AppStorage(\.openAIAPIKey) var openAIAPIKey: String
910
@AppStorage(\.chatGPTModel) var chatGPTModel: String
1011
@AppStorage(\.chatGPTEndpoint) var chatGPTEndpoint: String
@@ -30,68 +31,124 @@ struct OpenAIView: View {
3031

3132
Form {
3233
HStack {
33-
Text("OpenAI API Key")
3434
TextField(text: $settings.openAIAPIKey, prompt: Text("sk-*")) {
35-
EmptyView()
36-
}.textFieldStyle(.copilot)
35+
Text("OpenAI API Key")
36+
}.textFieldStyle(.roundedBorder)
3737
Button(action: {
3838
openURL(apiKeyURL)
3939
}) {
4040
Image(systemName: "questionmark.circle.fill")
41-
}
42-
.buttonStyle(.plain)
41+
}.buttonStyle(.plain)
4342
}
4443

4544
HStack {
46-
Text("ChatGPT Model")
47-
TextField(text: $settings.chatGPTModel, prompt: Text("gpt-3.5-turbo")) {
48-
EmptyView()
49-
}.textFieldStyle(.copilot)
50-
45+
Picker(selection: $settings.chatGPTModel) {
46+
if !settings.chatGPTModel.isEmpty,
47+
ChatGPTModel(rawValue: settings.chatGPTModel) == nil
48+
{
49+
Text(settings.chatGPTModel).tag(settings.chatGPTModel)
50+
}
51+
ForEach(ChatGPTModel.allCases, id: \.self) { model in
52+
Text(model.rawValue).tag(model.rawValue)
53+
}
54+
} label: {
55+
Text("ChatGPT Model")
56+
}.pickerStyle(.menu)
5157
Button(action: {
5258
openURL(modelURL)
5359
}) {
5460
Image(systemName: "questionmark.circle.fill")
61+
}.buttonStyle(.plain)
62+
}
63+
.onChange(of: settings.chatGPTModel) { newValue in
64+
if let model = ChatGPTModel(rawValue: newValue) {
65+
settings.chatGPTEndpoint = model.endpoint
5566
}
56-
.buttonStyle(.plain)
5767
}
5868

59-
HStack {
60-
Text("ChatGPT Endpoint")
61-
TextField(
62-
text: $settings.chatGPTEndpoint,
63-
prompt: Text("https://api.openai.com/v1/chat/completions")
64-
) {
65-
EmptyView()
66-
}.textFieldStyle(.copilot)
67-
}
68-
69-
HStack {
70-
Text("Reply in Language")
71-
TextField(
72-
text: $settings.chatGPTLanguage,
73-
prompt: Text("e.g. English. Leave it blank to let the bot decide.")
74-
) {
75-
EmptyView()
76-
}.textFieldStyle(.copilot)
69+
TextField(
70+
text: $settings.chatGPTEndpoint,
71+
prompt: Text("https://api.openai.com/v1/chat/completions")
72+
) {
73+
Text("ChatGPT Server")
74+
}.textFieldStyle(.roundedBorder)
75+
76+
if #available(macOS 13.0, *) {
77+
LabeledContent("Reply in Language") {
78+
languagePicker
79+
}
80+
} else {
81+
HStack {
82+
Text("Reply in Language")
83+
languagePicker
84+
}
7785
}
78-
79-
HStack {
80-
Text("Max Token")
81-
TextField(
82-
text: .init(get: {
83-
String(settings.chatGPTMaxToken)
84-
}, set: { newValue in
85-
settings.chatGPTMaxToken = Int(newValue) ?? 0
86-
})
87-
) {
88-
EmptyView()
89-
}.textFieldStyle(.copilot)
86+
87+
if let model = ChatGPTModel(rawValue: settings.chatGPTModel) {
88+
let binding = Binding(
89+
get: { String(settings.chatGPTMaxToken) },
90+
set: {
91+
if let selectionMaxToken = Int($0) {
92+
settings.chatGPTMaxToken = model
93+
.maxToken < selectionMaxToken ? model
94+
.maxToken : selectionMaxToken
95+
} else {
96+
settings.chatGPTMaxToken = 0
97+
}
98+
}
99+
)
100+
HStack {
101+
Stepper(
102+
value: $settings.chatGPTMaxToken,
103+
in: 0...model.maxToken,
104+
step: 1
105+
) {
106+
Text("Max Token")
107+
}
108+
TextField(text: binding) {
109+
EmptyView()
110+
}
111+
.labelsHidden()
112+
.textFieldStyle(.roundedBorder)
113+
}
90114
}
91115
}
92116
}
93117
}
94118
}
119+
120+
var languagePicker: some View {
121+
Menu {
122+
if !settings.chatGPTLanguage.isEmpty,
123+
!OpenAIViewSettings.availableLocalizedLocales
124+
.contains(settings.chatGPTLanguage)
125+
{
126+
Button(
127+
settings.chatGPTLanguage,
128+
action: { self.settings.chatGPTLanguage = settings.chatGPTLanguage }
129+
)
130+
}
131+
Button(
132+
"Auto-detected by ChatGPT",
133+
action: { self.settings.chatGPTLanguage = "" }
134+
)
135+
ForEach(
136+
OpenAIViewSettings.availableLocalizedLocales,
137+
id: \.self
138+
) { localizedLocales in
139+
Button(
140+
localizedLocales,
141+
action: { self.settings.chatGPTLanguage = localizedLocales }
142+
)
143+
}
144+
} label: {
145+
Text(
146+
settings.chatGPTLanguage.isEmpty
147+
? "Auto-detected by ChatGPT"
148+
: settings.chatGPTLanguage
149+
)
150+
}
151+
}
95152
}
96153

97154
struct OpenAIView_Previews: PreviewProvider {

Copilot for Xcode/SettingsView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ final class Settings: ObservableObject {
1515
var suggestionWidgetPositionMode: SuggestionWidgetPositionMode
1616
@AppStorage(\.widgetColorScheme)
1717
var widgetColorScheme: WidgetColorScheme
18+
@AppStorage(\.acceptSuggestionWithAccessibilityAPI)
19+
var acceptSuggestionWithAccessibilityAPI: Bool
1820
init() {}
1921
}
2022

@@ -107,6 +109,11 @@ struct SettingsView: View {
107109
.fill(Color.white.opacity(0.2))
108110
)
109111
}
112+
113+
Toggle(isOn: $settings.acceptSuggestionWithAccessibilityAPI) {
114+
Text("Use accessibility API to accept suggestion in widget")
115+
}
116+
.toggleStyle(.switch)
110117
}
111118
}.buttonStyle(.copilot)
112119
}

Core/Package.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ let package = Package(
3737
.package(url: "https://github.com/raspu/Highlightr", from: "2.1.0"),
3838
.package(url: "https://github.com/JohnSundell/Splash", from: "0.1.0"),
3939
.package(url: "https://github.com/nmdias/FeedKit", from: "9.1.2"),
40-
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"),
41-
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
40+
.package(url: "https://github.com/intitni/swift-markdown-ui", branch: "main"),
41+
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
4242
],
4343
targets: [
4444
.target(name: "CGEventObserver"),
@@ -86,6 +86,7 @@ let package = Package(
8686
"SuggestionWidget",
8787
"AXExtension",
8888
"Logger",
89+
"ChatService",
8990
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
9091
]
9192
),
@@ -147,5 +148,8 @@ let package = Package(
147148
dependencies: ["OpenAIService"]
148149
),
149150
.target(name: "Preferences"),
151+
.target(name: "ChatPlugins", dependencies: ["OpenAIService", "Environment", "Terminal"]),
152+
.target(name: "Terminal"),
153+
.target(name: "ChatService", dependencies: ["OpenAIService", "ChatPlugins", "Environment"]),
150154
]
151155
)

Core/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import AppKit
22

33
public final class ActiveApplicationMonitor {
44
static let shared = ActiveApplicationMonitor()
5-
var latestXcode: NSRunningApplication?
5+
var latestXcode: NSRunningApplication? = NSWorkspace.shared.runningApplications
6+
.first(where: \.isXcode)
67
var activeApplication = NSWorkspace.shared.runningApplications.first(where: \.isActive) {
78
didSet {
89
if activeApplication?.isXcode ?? false {
910
latestXcode = activeApplication
1011
}
1112
}
1213
}
14+
1315
private var continuations: [UUID: AsyncStream<NSRunningApplication?>.Continuation] = [:]
1416

1517
private init() {
@@ -40,7 +42,7 @@ public final class ActiveApplicationMonitor {
4042
}
4143
return nil
4244
}
43-
45+
4446
public static var latestXcode: NSRunningApplication? { shared.latestXcode }
4547

4648
public static func createStream() -> AsyncStream<NSRunningApplication?> {
@@ -72,7 +74,6 @@ public final class ActiveApplicationMonitor {
7274
}
7375
}
7476

75-
extension NSRunningApplication {
76-
public var isXcode: Bool { bundleIdentifier == "com.apple.dt.Xcode" }
77+
public extension NSRunningApplication {
78+
var isXcode: Bool { bundleIdentifier == "com.apple.dt.Xcode" }
7779
}
78-
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Foundation
2+
import OpenAIService
3+
4+
public protocol ChatPlugin {
5+
/// Should be [a-zA-Z0-9]+
6+
static var command: String { get }
7+
var name: String { get }
8+
9+
init(inside chatGPTService: ChatGPTServiceType, delegate: ChatPluginDelegate)
10+
func send(content: String) async
11+
func cancel() async
12+
func stopResponding() async
13+
}
14+
15+
public protocol ChatPluginDelegate: AnyObject {
16+
func pluginDidStart(_ plugin: ChatPlugin)
17+
func pluginDidEnd(_ plugin: ChatPlugin)
18+
func pluginDidStartResponding(_ plugin: ChatPlugin)
19+
func pluginDidEndResponding(_ plugin: ChatPlugin)
20+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import Environment
2+
import Foundation
3+
import OpenAIService
4+
import Terminal
5+
6+
public actor TerminalChatPlugin: ChatPlugin {
7+
public static var command: String { "run" }
8+
public nonisolated var name: String { "Terminal" }
9+
10+
let chatGPTService: ChatGPTServiceType
11+
var terminal: TerminalType = Terminal()
12+
var isCancelled = false
13+
weak var delegate: ChatPluginDelegate?
14+
15+
public init(inside chatGPTService: ChatGPTServiceType, delegate: ChatPluginDelegate) {
16+
self.chatGPTService = chatGPTService
17+
self.delegate = delegate
18+
}
19+
20+
public func send(content: String) async {
21+
delegate?.pluginDidStart(self)
22+
delegate?.pluginDidStartResponding(self)
23+
24+
let id = "\(Self.command)-\(UUID().uuidString)"
25+
var message = ChatMessage(id: id, role: .assistant, content: "")
26+
var outputContent = "" {
27+
didSet {
28+
message.content = """
29+
```
30+
\(outputContent)
31+
```
32+
"""
33+
}
34+
}
35+
36+
do {
37+
let fileURL = try await Environment.fetchCurrentFileURL()
38+
let projectURL = try await Environment.fetchCurrentProjectRootURL(fileURL)
39+
40+
await chatGPTService.mutateHistory { history in
41+
history.append(.init(role: .user, content: "Run command: \(content)"))
42+
}
43+
44+
if isCancelled { throw CancellationError() }
45+
46+
let output = terminal.streamCommand(
47+
"/bin/bash",
48+
arguments: ["-c", content],
49+
currentDirectoryPath: projectURL?.path ?? fileURL.path,
50+
environment: [
51+
"PROJECT_ROOT": projectURL?.path ?? fileURL.path,
52+
"FILE_PATH": fileURL.path,
53+
]
54+
)
55+
56+
for try await content in output {
57+
if isCancelled { throw CancellationError() }
58+
await chatGPTService.mutateHistory { history in
59+
if history.last?.id == id {
60+
history.removeLast()
61+
}
62+
outputContent += content
63+
history.append(message)
64+
}
65+
}
66+
outputContent += "\n[finished]"
67+
await chatGPTService.mutateHistory { history in
68+
if history.last?.id == id {
69+
history.removeLast()
70+
}
71+
history.append(message)
72+
}
73+
} catch let error as Terminal.TerminationError {
74+
outputContent += "\n[error: \(error.status)]"
75+
await chatGPTService.mutateHistory { history in
76+
if history.last?.id == id {
77+
history.removeLast()
78+
}
79+
history.append(message)
80+
}
81+
} catch {
82+
outputContent += "\n[error: \(error.localizedDescription)]"
83+
await chatGPTService.mutateHistory { history in
84+
if history.last?.id == id {
85+
history.removeLast()
86+
}
87+
history.append(message)
88+
}
89+
}
90+
91+
delegate?.pluginDidEndResponding(self)
92+
delegate?.pluginDidEnd(self)
93+
}
94+
95+
public func cancel() async {
96+
isCancelled = true
97+
await terminal.terminate()
98+
}
99+
100+
public func stopResponding() async {
101+
isCancelled = true
102+
await terminal.terminate()
103+
}
104+
}

0 commit comments

Comments
 (0)