Skip to content

Commit 5f25b7c

Browse files
committed
Add ShortcutChatPlugin
1 parent b9966db commit 5f25b7c

3 files changed

Lines changed: 154 additions & 0 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+

0 commit comments

Comments
 (0)