Skip to content

Commit 8208587

Browse files
committed
Merge branch 'release/0.18.1'
2 parents a94e308 + a115353 commit 8208587

17 files changed

Lines changed: 355 additions & 67 deletions

File tree

Core/Package.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ let package = Package(
5050
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
5151
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"),
5252
.package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"),
53+
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"),
5354
],
5455
targets: [
5556
// MARK: - Main
@@ -182,6 +183,7 @@ let package = Package(
182183
// plugins
183184
"MathChatPlugin",
184185
"SearchChatPlugin",
186+
"ShortcutChatPlugin",
185187

186188
.product(name: "OpenAIService", package: "Tool"),
187189
.product(name: "Preferences", package: "Tool"),
@@ -315,6 +317,16 @@ let package = Package(
315317
],
316318
path: "Sources/ChatPlugins/SearchChatPlugin"
317319
),
320+
321+
.target(
322+
name: "ShortcutChatPlugin",
323+
dependencies: [
324+
"ChatPlugin",
325+
.product(name: "Parsing", package: "swift-parsing"),
326+
.product(name: "Terminal", package: "Tool"),
327+
],
328+
path: "Sources/ChatPlugins/ShortcutChatPlugin"
329+
),
318330
]
319331
)
320332

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import ChatPlugin
2+
import Environment
3+
import Foundation
4+
import OpenAIService
5+
import Parsing
6+
import Terminal
7+
8+
public actor ShortcutChatPlugin: ChatPlugin {
9+
public static var command: String { "shortcut" }
10+
public nonisolated var name: String { "Shortcut" }
11+
12+
let chatGPTService: any ChatGPTServiceType
13+
var terminal: TerminalType = Terminal()
14+
var isCancelled = false
15+
weak var delegate: ChatPluginDelegate?
16+
17+
public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) {
18+
self.chatGPTService = chatGPTService
19+
self.delegate = delegate
20+
}
21+
22+
public func send(content: String, originalMessage: String) async {
23+
delegate?.pluginDidStart(self)
24+
delegate?.pluginDidStartResponding(self)
25+
26+
defer {
27+
delegate?.pluginDidEndResponding(self)
28+
delegate?.pluginDidEnd(self)
29+
}
30+
31+
let id = "\(Self.command)-\(UUID().uuidString)"
32+
var message = ChatMessage(id: id, role: .assistant, content: "")
33+
34+
var content = content[...]
35+
let firstParenthesisParser = PrefixThrough("(")
36+
let shortcutNameParser = PrefixUpTo(")")
37+
38+
_ = try? firstParenthesisParser.parse(&content)
39+
let shortcutName = try? shortcutNameParser.parse(&content)
40+
_ = try? PrefixThrough(")").parse(&content)
41+
42+
guard let shortcutName, !shortcutName.isEmpty else {
43+
message.content =
44+
"Please provide the shortcut name in format: `/\(Self.command)(shortcut name)`."
45+
await chatGPTService.mutateHistory { history in
46+
history.append(message)
47+
}
48+
return
49+
}
50+
51+
var input = String(content).trimmingCharacters(in: .whitespacesAndNewlines)
52+
if input.isEmpty {
53+
// if no input detected, use the previous message as input
54+
input = await chatGPTService.history.last?.content ?? ""
55+
await chatGPTService.mutateHistory { history in
56+
history.append(.init(role: .user, content: originalMessage))
57+
}
58+
} else {
59+
await chatGPTService.mutateHistory { history in
60+
history.append(.init(role: .user, content: originalMessage))
61+
}
62+
}
63+
64+
do {
65+
if isCancelled { throw CancellationError() }
66+
67+
let env = ProcessInfo.processInfo.environment
68+
let shell = env["SHELL"] ?? "/bin/bash"
69+
let temporaryURL = FileManager.default.temporaryDirectory
70+
let temporaryInputFileURL = temporaryURL
71+
.appendingPathComponent("\(id)-input.txt")
72+
let temporaryOutputFileURL = temporaryURL
73+
.appendingPathComponent("\(id)-output")
74+
75+
try input.write(to: temporaryInputFileURL, atomically: true, encoding: .utf8)
76+
77+
let command = """
78+
shortcuts run "\(shortcutName)" \
79+
-i "\(temporaryInputFileURL.path)" \
80+
-o "\(temporaryOutputFileURL.path)"
81+
"""
82+
83+
_ = try await terminal.runCommand(
84+
shell,
85+
arguments: ["-i", "-l", "-c", command],
86+
currentDirectoryPath: "/",
87+
environment: [:]
88+
)
89+
90+
await Task.yield()
91+
92+
if FileManager.default.fileExists(atPath: temporaryOutputFileURL.path) {
93+
let data = try Data(contentsOf: temporaryOutputFileURL)
94+
if let text = String(data: data, encoding: .utf8) {
95+
message.content = text
96+
if text.isEmpty {
97+
message.content = "Finished"
98+
}
99+
await chatGPTService.mutateHistory { history in
100+
history.append(message)
101+
}
102+
} else {
103+
message.content = """
104+
[View File](\(temporaryOutputFileURL))
105+
"""
106+
await chatGPTService.mutateHistory { history in
107+
history.append(message)
108+
}
109+
}
110+
111+
return
112+
}
113+
114+
message.content = "Finished"
115+
await chatGPTService.mutateHistory { history in
116+
history.append(message)
117+
}
118+
} catch {
119+
message.content = error.localizedDescription
120+
if error.localizedDescription.isEmpty {
121+
message.content = "Error"
122+
}
123+
await chatGPTService.mutateHistory { history in
124+
history.append(message)
125+
}
126+
}
127+
}
128+
129+
public func cancel() async {
130+
isCancelled = true
131+
await terminal.terminate()
132+
}
133+
134+
public func stopResponding() async {
135+
isCancelled = true
136+
await terminal.terminate()
137+
}
138+
}
139+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import ChatPlugin
22
import MathChatPlugin
33
import SearchChatPlugin
4+
import ShortcutChatPlugin
45

56
let allPlugins: [ChatPlugin.Type] = [
67
TerminalChatPlugin.self,
78
AITerminalChatPlugin.self,
89
MathChatPlugin.self,
910
SearchChatPlugin.self,
11+
ShortcutChatPlugin.self,
1012
]
13+

Core/Sources/GitHubCopilotService/GitHubCopilotRequest.swift

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import SuggestionModel
21
import Foundation
32
import JSONRPC
43
import LanguageServerProtocol
4+
import SuggestionModel
55

66
struct GitHubCopilotDoc: Codable {
77
var source: String
@@ -26,8 +26,49 @@ enum GitHubCopilotRequest {
2626
struct SetEditorInfo: GitHubCopilotRequestType {
2727
struct Response: Codable {}
2828

29+
var networkProxy: JSONValue? {
30+
let host = UserDefaults.shared.value(for: \.gitHubCopilotProxyHost)
31+
if host.isEmpty { return nil }
32+
var port = UserDefaults.shared.value(for: \.gitHubCopilotProxyPort)
33+
if port.isEmpty { port = "80" }
34+
let username = UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername)
35+
if username.isEmpty {
36+
return .hash([
37+
"host": .string(host),
38+
"port": .number(Double(Int(port) ?? 80)),
39+
"rejectUnauthorized": .bool(UserDefaults.shared
40+
.value(for: \.gitHubCopilotUseStrictSSL)),
41+
])
42+
} else {
43+
return .hash([
44+
"host": .string(host),
45+
"port": .number(Double(Int(port) ?? 80)),
46+
"rejectUnauthorized": .bool(UserDefaults.shared
47+
.value(for: \.gitHubCopilotUseStrictSSL)),
48+
"username": .string(username),
49+
"password": .string(UserDefaults.shared
50+
.value(for: \.gitHubCopilotProxyPassword)),
51+
52+
])
53+
}
54+
}
55+
2956
var request: ClientRequest {
30-
.custom("setEditorInfo", .hash([
57+
if let networkProxy {
58+
return .custom("setEditorInfo", .hash([
59+
"editorInfo": .hash([
60+
"name": "Xcode",
61+
"version": "",
62+
]),
63+
"editorPluginInfo": .hash([
64+
"name": "Copilot for Xcode",
65+
"version": "",
66+
]),
67+
"networkProxy": networkProxy,
68+
]))
69+
}
70+
71+
return .custom("setEditorInfo", .hash([
3172
"editorInfo": .hash([
3273
"name": "Xcode",
3374
"version": "",
@@ -171,3 +212,4 @@ enum GitHubCopilotRequest {
171212
}
172213
}
173214
}
215+

Core/Sources/GitHubCopilotService/GitHubCopilotService.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,13 @@ public class GitHubCopilotBaseService {
165165

166166
self.server = server
167167
localProcessServer = localServer
168+
169+
Task {
170+
try await server.sendRequest(GitHubCopilotRequest.SetEditorInfo())
171+
}
168172
}
173+
174+
169175

170176
public static func createFoldersIfNeeded() throws -> (
171177
applicationSupportURL: URL,
@@ -211,9 +217,6 @@ public final class GitHubCopilotAuthService: GitHubCopilotBaseService,
211217
public init() throws {
212218
let home = FileManager.default.homeDirectoryForCurrentUser
213219
try super.init(projectRootURL: home)
214-
Task {
215-
try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo())
216-
}
217220
}
218221

219222
public func checkStatus() async throws -> GitHubCopilotAccountStatus {

Core/Sources/HostApp/AccountSettings/CopilotView.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ struct CopilotView: View {
1313
@AppStorage(\.runNodeWith) var runNodeWith
1414
@AppStorage("username") var username: String = ""
1515
@AppStorage(\.gitHubCopilotVerboseLog) var gitHubCopilotVerboseLog
16+
@AppStorage(\.gitHubCopilotProxyHost) var gitHubCopilotProxyHost
17+
@AppStorage(\.gitHubCopilotProxyPort) var gitHubCopilotProxyPort
18+
@AppStorage(\.gitHubCopilotProxyUsername) var gitHubCopilotProxyUsername
19+
@AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword
20+
@AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL
1621

1722
init() {}
1823
}
@@ -182,9 +187,9 @@ struct CopilotView: View {
182187
uninstallButton
183188
}
184189
}
185-
190+
186191
Text("Language Server Version: \(version ?? "Loading..")")
187-
192+
188193
Text("Status: \(status?.description ?? "Loading..")")
189194

190195
HStack(alignment: .center) {
@@ -226,6 +231,24 @@ struct CopilotView: View {
226231
Form {
227232
Toggle("Verbose Log", isOn: $settings.gitHubCopilotVerboseLog)
228233
}
234+
235+
Divider()
236+
237+
Form {
238+
TextField(text: $settings.gitHubCopilotProxyHost, prompt: Text("xxx.xxx.xxx.xxx, leave it blank to disable proxy.")) {
239+
Text("Proxy Host")
240+
}
241+
TextField(text: $settings.gitHubCopilotProxyPort, prompt: Text("80")) {
242+
Text("Proxy Port")
243+
}
244+
TextField(text: $settings.gitHubCopilotProxyUsername) {
245+
Text("Proxy Username")
246+
}
247+
SecureField(text: $settings.gitHubCopilotProxyPassword) {
248+
Text("Proxy Password")
249+
}
250+
Toggle("Proxy Strict SSL", isOn: $settings.gitHubCopilotUseStrictSSL)
251+
}
229252
}
230253
Spacer()
231254
}.onAppear {

Core/Sources/HostApp/DebugView.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import Preferences
22
import SwiftUI
33

44
final class DebugSettings: ObservableObject {
5-
@AppStorage(\.disableLazyVStack) var disableLazyVStack
5+
@AppStorage(\.animationACrashSuggestion) var animationACrashSuggestion
6+
@AppStorage(\.animationBCrashSuggestion) var animationBCrashSuggestion
7+
@AppStorage(\.animationCCrashSuggestion) var animationCCrashSuggestion
68
@AppStorage(\.preCacheOnFileOpen) var preCacheOnFileOpen
79
@AppStorage(\.useCustomScrollViewWorkaround) var useCustomScrollViewWorkaround
810
@AppStorage(\.triggerActionWithAccessibilityAPI) var triggerActionWithAccessibilityAPI
@@ -15,8 +17,14 @@ struct DebugSettingsView: View {
1517
var body: some View {
1618
ScrollView {
1719
Form {
18-
Toggle(isOn: $settings.disableLazyVStack) {
19-
Text("Disable LazyVStack")
20+
Toggle(isOn: $settings.animationACrashSuggestion) {
21+
Text("Enable Animation A")
22+
}
23+
Toggle(isOn: $settings.animationBCrashSuggestion) {
24+
Text("Enable Animation B")
25+
}
26+
Toggle(isOn: $settings.animationCCrashSuggestion) {
27+
Text("Enable Widget Breathing Animation")
2028
}
2129
Toggle(isOn: $settings.preCacheOnFileOpen) {
2230
Text("Cache editor information on file open")

Core/Sources/SuggestionWidget/CustomScrollView/CustomScrollView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ struct CustomScrollView<Content: View>: View {
4040
.listStyle(.plain)
4141
.frame(idealHeight: max(10, height))
4242
.onPreferenceChange(CustomScrollViewHeightPreferenceKey.self) { newHeight in
43-
height = newHeight
43+
Task { @MainActor in
44+
height = newHeight
45+
}
4446
}
4547
} else {
4648
ScrollView {
@@ -49,3 +51,4 @@ struct CustomScrollView<Content: View>: View {
4951
}
5052
}
5153
}
54+

Core/Sources/SuggestionWidget/CustomTextEditor.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct CustomTextEditor: NSViewRepresentable {
3030
let textView = (context.coordinator.theTextView.documentView as! NSTextView)
3131
guard textView.string != text else { return }
3232
textView.string = text
33+
textView.undoManager?.removeAllActions()
3334
}
3435
}
3536

Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ struct ChatPanelInputArea: View {
362362
"/airun",
363363
"/math",
364364
"/search",
365+
"/shortcut",
365366
"/exit",
366367
"@selection",
367368
"@file",

0 commit comments

Comments
 (0)