Skip to content
Prev Previous commit
Next Next commit
Add ShortcutChatPlugin
  • Loading branch information
intitni committed Jun 10, 2023
commit 5f25b7cf59589a968e71a5f94bf1dc9eea2dfd19
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,
]