Skip to content
12 changes: 12 additions & 0 deletions Core/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ let package = Package(
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"),
.package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"),
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"),
],
targets: [
// MARK: - Main
Expand Down Expand Up @@ -182,6 +183,7 @@ let package = Package(
// plugins
"MathChatPlugin",
"SearchChatPlugin",
"ShortcutChatPlugin",

.product(name: "OpenAIService", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
Expand Down Expand Up @@ -315,6 +317,16 @@ let package = Package(
],
path: "Sources/ChatPlugins/SearchChatPlugin"
),

.target(
name: "ShortcutChatPlugin",
dependencies: [
"ChatPlugin",
.product(name: "Parsing", package: "swift-parsing"),
.product(name: "Terminal", package: "Tool"),
],
path: "Sources/ChatPlugins/ShortcutChatPlugin"
),
]
)

139 changes: 139 additions & 0 deletions Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutChatPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import ChatPlugin
import Environment
import Foundation
import OpenAIService
import Parsing
import Terminal

public actor ShortcutChatPlugin: ChatPlugin {
public static var command: String { "shortcut" }
public nonisolated var name: String { "Shortcut" }

let chatGPTService: any ChatGPTServiceType
var terminal: TerminalType = Terminal()
var isCancelled = false
weak var delegate: ChatPluginDelegate?

public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) {
self.chatGPTService = chatGPTService
self.delegate = delegate
}

public func send(content: String, originalMessage: String) async {
delegate?.pluginDidStart(self)
delegate?.pluginDidStartResponding(self)

defer {
delegate?.pluginDidEndResponding(self)
delegate?.pluginDidEnd(self)
}

let id = "\(Self.command)-\(UUID().uuidString)"
var message = ChatMessage(id: id, role: .assistant, content: "")

var content = content[...]
let firstParenthesisParser = PrefixThrough("(")
let shortcutNameParser = PrefixUpTo(")")

_ = try? firstParenthesisParser.parse(&content)
let shortcutName = try? shortcutNameParser.parse(&content)
_ = try? PrefixThrough(")").parse(&content)

guard let shortcutName, !shortcutName.isEmpty else {
message.content =
"Please provide the shortcut name in format: `/\(Self.command)(shortcut name)`."
await chatGPTService.mutateHistory { history in
history.append(message)
}
return
}

var input = String(content).trimmingCharacters(in: .whitespacesAndNewlines)
if input.isEmpty {
// if no input detected, use the previous message as input
input = await chatGPTService.history.last?.content ?? ""
await chatGPTService.mutateHistory { history in
history.append(.init(role: .user, content: originalMessage))
}
} else {
await chatGPTService.mutateHistory { history in
history.append(.init(role: .user, content: originalMessage))
}
}

do {
if isCancelled { throw CancellationError() }

let env = ProcessInfo.processInfo.environment
let shell = env["SHELL"] ?? "/bin/bash"
let temporaryURL = FileManager.default.temporaryDirectory
let temporaryInputFileURL = temporaryURL
.appendingPathComponent("\(id)-input.txt")
let temporaryOutputFileURL = temporaryURL
.appendingPathComponent("\(id)-output")

try input.write(to: temporaryInputFileURL, atomically: true, encoding: .utf8)

let command = """
shortcuts run "\(shortcutName)" \
-i "\(temporaryInputFileURL.path)" \
-o "\(temporaryOutputFileURL.path)"
"""

_ = try await terminal.runCommand(
shell,
arguments: ["-i", "-l", "-c", command],
currentDirectoryPath: "/",
environment: [:]
)

await Task.yield()

if FileManager.default.fileExists(atPath: temporaryOutputFileURL.path) {
let data = try Data(contentsOf: temporaryOutputFileURL)
if let text = String(data: data, encoding: .utf8) {
message.content = text
if text.isEmpty {
message.content = "Finished"
}
await chatGPTService.mutateHistory { history in
history.append(message)
}
} else {
message.content = """
[View File](\(temporaryOutputFileURL))
"""
await chatGPTService.mutateHistory { history in
history.append(message)
}
}

return
}

message.content = "Finished"
await chatGPTService.mutateHistory { history in
history.append(message)
}
} catch {
message.content = error.localizedDescription
if error.localizedDescription.isEmpty {
message.content = "Error"
}
await chatGPTService.mutateHistory { history in
history.append(message)
}
}
}

public func cancel() async {
isCancelled = true
await terminal.terminate()
}

public func stopResponding() async {
isCancelled = true
await terminal.terminate()
}
}

3 changes: 3 additions & 0 deletions Core/Sources/ChatService/AllPlugins.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import ChatPlugin
import MathChatPlugin
import SearchChatPlugin
import ShortcutChatPlugin

let allPlugins: [ChatPlugin.Type] = [
TerminalChatPlugin.self,
AITerminalChatPlugin.self,
MathChatPlugin.self,
SearchChatPlugin.self,
ShortcutChatPlugin.self,
]

46 changes: 44 additions & 2 deletions Core/Sources/GitHubCopilotService/GitHubCopilotRequest.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SuggestionModel
import Foundation
import JSONRPC
import LanguageServerProtocol
import SuggestionModel

struct GitHubCopilotDoc: Codable {
var source: String
Expand All @@ -26,8 +26,49 @@ enum GitHubCopilotRequest {
struct SetEditorInfo: GitHubCopilotRequestType {
struct Response: Codable {}

var networkProxy: JSONValue? {
let host = UserDefaults.shared.value(for: \.gitHubCopilotProxyHost)
if host.isEmpty { return nil }
var port = UserDefaults.shared.value(for: \.gitHubCopilotProxyPort)
if port.isEmpty { port = "80" }
let username = UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername)
if username.isEmpty {
return .hash([
"host": .string(host),
"port": .number(Double(Int(port) ?? 80)),
"rejectUnauthorized": .bool(UserDefaults.shared
.value(for: \.gitHubCopilotUseStrictSSL)),
])
} else {
return .hash([
"host": .string(host),
"port": .number(Double(Int(port) ?? 80)),
"rejectUnauthorized": .bool(UserDefaults.shared
.value(for: \.gitHubCopilotUseStrictSSL)),
"username": .string(username),
"password": .string(UserDefaults.shared
.value(for: \.gitHubCopilotProxyPassword)),

])
}
}

var request: ClientRequest {
.custom("setEditorInfo", .hash([
if let networkProxy {
return .custom("setEditorInfo", .hash([
"editorInfo": .hash([
"name": "Xcode",
"version": "",
]),
"editorPluginInfo": .hash([
"name": "Copilot for Xcode",
"version": "",
]),
"networkProxy": networkProxy,
]))
}

return .custom("setEditorInfo", .hash([
"editorInfo": .hash([
"name": "Xcode",
"version": "",
Expand Down Expand Up @@ -171,3 +212,4 @@ enum GitHubCopilotRequest {
}
}
}

9 changes: 6 additions & 3 deletions Core/Sources/GitHubCopilotService/GitHubCopilotService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,13 @@ public class GitHubCopilotBaseService {

self.server = server
localProcessServer = localServer

Task {
try await server.sendRequest(GitHubCopilotRequest.SetEditorInfo())
}
}



public static func createFoldersIfNeeded() throws -> (
applicationSupportURL: URL,
Expand Down Expand Up @@ -211,9 +217,6 @@ public final class GitHubCopilotAuthService: GitHubCopilotBaseService,
public init() throws {
let home = FileManager.default.homeDirectoryForCurrentUser
try super.init(projectRootURL: home)
Task {
try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo())
}
}

public func checkStatus() async throws -> GitHubCopilotAccountStatus {
Expand Down
27 changes: 25 additions & 2 deletions Core/Sources/HostApp/AccountSettings/CopilotView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ struct CopilotView: View {
@AppStorage(\.runNodeWith) var runNodeWith
@AppStorage("username") var username: String = ""
@AppStorage(\.gitHubCopilotVerboseLog) var gitHubCopilotVerboseLog
@AppStorage(\.gitHubCopilotProxyHost) var gitHubCopilotProxyHost
@AppStorage(\.gitHubCopilotProxyPort) var gitHubCopilotProxyPort
@AppStorage(\.gitHubCopilotProxyUsername) var gitHubCopilotProxyUsername
@AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword
@AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL

init() {}
}
Expand Down Expand Up @@ -182,9 +187,9 @@ struct CopilotView: View {
uninstallButton
}
}

Text("Language Server Version: \(version ?? "Loading..")")

Text("Status: \(status?.description ?? "Loading..")")

HStack(alignment: .center) {
Expand Down Expand Up @@ -226,6 +231,24 @@ struct CopilotView: View {
Form {
Toggle("Verbose Log", isOn: $settings.gitHubCopilotVerboseLog)
}

Divider()

Form {
TextField(text: $settings.gitHubCopilotProxyHost, prompt: Text("xxx.xxx.xxx.xxx, leave it blank to disable proxy.")) {
Text("Proxy Host")
}
TextField(text: $settings.gitHubCopilotProxyPort, prompt: Text("80")) {
Text("Proxy Port")
}
TextField(text: $settings.gitHubCopilotProxyUsername) {
Text("Proxy Username")
}
SecureField(text: $settings.gitHubCopilotProxyPassword) {
Text("Proxy Password")
}
Toggle("Proxy Strict SSL", isOn: $settings.gitHubCopilotUseStrictSSL)
}
}
Spacer()
}.onAppear {
Expand Down
14 changes: 11 additions & 3 deletions Core/Sources/HostApp/DebugView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import Preferences
import SwiftUI

final class DebugSettings: ObservableObject {
@AppStorage(\.disableLazyVStack) var disableLazyVStack
@AppStorage(\.animationACrashSuggestion) var animationACrashSuggestion
@AppStorage(\.animationBCrashSuggestion) var animationBCrashSuggestion
@AppStorage(\.animationCCrashSuggestion) var animationCCrashSuggestion
@AppStorage(\.preCacheOnFileOpen) var preCacheOnFileOpen
@AppStorage(\.useCustomScrollViewWorkaround) var useCustomScrollViewWorkaround
@AppStorage(\.triggerActionWithAccessibilityAPI) var triggerActionWithAccessibilityAPI
Expand All @@ -15,8 +17,14 @@ struct DebugSettingsView: View {
var body: some View {
ScrollView {
Form {
Toggle(isOn: $settings.disableLazyVStack) {
Text("Disable LazyVStack")
Toggle(isOn: $settings.animationACrashSuggestion) {
Text("Enable Animation A")
}
Toggle(isOn: $settings.animationBCrashSuggestion) {
Text("Enable Animation B")
}
Toggle(isOn: $settings.animationCCrashSuggestion) {
Text("Enable Widget Breathing Animation")
}
Toggle(isOn: $settings.preCacheOnFileOpen) {
Text("Cache editor information on file open")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ struct CustomScrollView<Content: View>: View {
.listStyle(.plain)
.frame(idealHeight: max(10, height))
.onPreferenceChange(CustomScrollViewHeightPreferenceKey.self) { newHeight in
height = newHeight
Task { @MainActor in
height = newHeight
}
}
} else {
ScrollView {
Expand All @@ -49,3 +51,4 @@ struct CustomScrollView<Content: View>: View {
}
}
}

1 change: 1 addition & 0 deletions Core/Sources/SuggestionWidget/CustomTextEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct CustomTextEditor: NSViewRepresentable {
let textView = (context.coordinator.theTextView.documentView as! NSTextView)
guard textView.string != text else { return }
textView.string = text
textView.undoManager?.removeAllActions()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ struct ChatPanelInputArea: View {
"/airun",
"/math",
"/search",
"/shortcut",
"/exit",
"@selection",
"@file",
Expand Down
Loading