Skip to content

Commit 3aec08a

Browse files
committed
Merge branch 'release/0.11.0'
2 parents 0ea2342 + b7525a7 commit 3aec08a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1631
-748
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.

Core/Package.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ let package = Package(
3535
.package(url: "https://github.com/ChimeHQ/LanguageClient", from: "0.3.1"),
3636
.package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"),
3737
.package(url: "https://github.com/raspu/Highlightr", from: "2.1.0"),
38-
.package(url: "https://github.com/JohnSundell/Splash", from: "0.1.0"),
38+
.package(url: "https://github.com/JohnSundell/Splash", branch: "master"),
3939
.package(url: "https://github.com/nmdias/FeedKit", from: "9.1.2"),
4040
.package(url: "https://github.com/intitni/swift-markdown-ui", branch: "main"),
41-
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
41+
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
4242
],
4343
targets: [
4444
.target(name: "CGEventObserver"),
@@ -123,9 +123,11 @@ let package = Package(
123123
"Environment",
124124
"Highlightr",
125125
"Splash",
126+
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
126127
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
127128
]
128129
),
130+
.testTarget(name: "SuggestionWidgetTests", dependencies: ["SuggestionWidget"]),
129131
.target(
130132
name: "UpdateChecker",
131133
dependencies: [
@@ -140,6 +142,7 @@ let package = Package(
140142
name: "OpenAIService",
141143
dependencies: [
142144
"Logger",
145+
"Preferences",
143146
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
144147
]
145148
),
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import Environment
2+
import Foundation
3+
import OpenAIService
4+
import Terminal
5+
6+
public actor AITerminalChatPlugin: ChatPlugin {
7+
public static var command: String { "airun" }
8+
public nonisolated var name: String { "AI Terminal" }
9+
10+
let chatGPTService: any ChatGPTServiceType
11+
var terminal: TerminalType = Terminal()
12+
var isCancelled = false
13+
weak var delegate: ChatPluginDelegate?
14+
var isStarted = false
15+
var command: String?
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) async {
23+
if !isStarted {
24+
isStarted = true
25+
delegate?.pluginDidStart(self)
26+
}
27+
28+
do {
29+
if let command {
30+
await chatGPTService.mutateHistory { history in
31+
history.append(.init(role: .user, content: content))
32+
}
33+
delegate?.pluginDidStartResponding(self)
34+
if isCancelled { return }
35+
switch try await checkConfirmation(content: content) {
36+
case .confirmation:
37+
delegate?.pluginDidEndResponding(self)
38+
delegate?.pluginDidEnd(self)
39+
delegate?.shouldStartAnotherPlugin(
40+
TerminalChatPlugin.self,
41+
withContent: command
42+
)
43+
case .cancellation:
44+
delegate?.pluginDidEndResponding(self)
45+
delegate?.pluginDidEnd(self)
46+
await chatGPTService.mutateHistory { history in
47+
history.append(.init(role: .assistant, content: "Cancelled"))
48+
}
49+
case .modification:
50+
let result = try await modifyCommand(command: command, requirement: content)
51+
self.command = result
52+
delegate?.pluginDidEndResponding(self)
53+
await chatGPTService.mutateHistory { history in
54+
history.append(.init(role: .assistant, content: """
55+
Confirm to run?
56+
```
57+
\(result)
58+
```
59+
"""))
60+
}
61+
case .other:
62+
delegate?.pluginDidEndResponding(self)
63+
await chatGPTService.mutateHistory { history in
64+
history.append(.init(
65+
role: .assistant,
66+
content: "Should I run it? Or should I modify it?"
67+
))
68+
}
69+
}
70+
} else {
71+
await chatGPTService.mutateHistory { history in
72+
history.append(.init(role: .user, content: "Run a command to \(content)"))
73+
}
74+
delegate?.pluginDidStartResponding(self)
75+
let result = try await generateCommand(task: content)
76+
command = result
77+
if isCancelled { return }
78+
await chatGPTService.mutateHistory { history in
79+
history.append(.init(role: .assistant, content: """
80+
Confirm to run?
81+
```
82+
\(result)
83+
```
84+
"""))
85+
}
86+
delegate?.pluginDidEndResponding(self)
87+
}
88+
} catch {
89+
await chatGPTService.mutateHistory { history in
90+
history.append(.init(role: .assistant, content: error.localizedDescription))
91+
}
92+
delegate?.pluginDidEndResponding(self)
93+
delegate?.pluginDidEnd(self)
94+
}
95+
}
96+
97+
public func cancel() async {
98+
isCancelled = true
99+
delegate?.pluginDidEndResponding(self)
100+
delegate?.pluginDidEnd(self)
101+
}
102+
103+
public func stopResponding() async {}
104+
105+
func generateCommand(task: String) async throws -> String {
106+
let p = """
107+
Available environment variables:
108+
- $PROJECT_ROOT: the root path of the project
109+
- $FILE_PATH: the currently editing file
110+
111+
Current directory path is the project root.
112+
113+
Generate a terminal command to solve the given task on macOS. If one command is not enough, you can use && to concatenate multiple commands.
114+
115+
The reply should contains only the command and nothing else.
116+
"""
117+
118+
return extractCodeFromMarkdown(try await askChatGPT(
119+
systemPrompt: p,
120+
question: "the task is: \"\(task)\""
121+
))
122+
}
123+
124+
func modifyCommand(command: String, requirement: String) async throws -> String {
125+
let p = """
126+
Available environment variables:
127+
- $PROJECT_ROOT: the root path of the project
128+
- $FILE_PATH: the currently editing file
129+
130+
Current directory path is the project root.
131+
132+
Modify the terminal command `\(
133+
command
134+
)` in macOS with the given requirement. If one command is not enough, you can use && to concatenate multiple commands.
135+
136+
The reply should contains only the command and nothing else.
137+
"""
138+
139+
return extractCodeFromMarkdown(try await askChatGPT(
140+
systemPrompt: p,
141+
question: "The requirement is: \"\(requirement)\""
142+
))
143+
}
144+
145+
func checkConfirmation(content: String) async throws -> Tone {
146+
let p = """
147+
Check the tone of the content, reply with only the number representing the tone.
148+
149+
1: If the given content is a phrase or sentence that considered a confirmation to run a command.
150+
151+
For example: "Yes", "Confirm", "True", "Run it". It can be in any language.
152+
153+
2: If the given content is a phrase or sentence that considered a cancellation to run a command.
154+
155+
For example: "No", "Cancel", "False", "Don't run it", "Stop". It can be in any language.
156+
157+
3: If the given content is a modification request.
158+
159+
For example: "Use echo instead", "Remove the argument", "Change to path".
160+
161+
4: Everything else.
162+
"""
163+
164+
let result = try await askChatGPT(
165+
systemPrompt: p,
166+
question: "The content is: \"\(content)\""
167+
)
168+
return Tone(rawValue: Int(result) ?? 2) ?? .cancellation
169+
}
170+
171+
enum Tone: Int {
172+
case confirmation = 1
173+
case cancellation = 2
174+
case modification = 3
175+
case other = 4
176+
}
177+
178+
func extractCodeFromMarkdown(_ markdown: String) -> String {
179+
let codeBlockRegex = try! NSRegularExpression(
180+
pattern: "```[\n](.*?)[\n]```",
181+
options: .dotMatchesLineSeparators
182+
)
183+
let range = NSRange(markdown.startIndex..<markdown.endIndex, in: markdown)
184+
guard let match = codeBlockRegex.firstMatch(in: markdown, options: [], range: range) else {
185+
return markdown
186+
.replacingOccurrences(of: "`", with: "")
187+
.replacingOccurrences(of: "\n", with: "")
188+
}
189+
let codeBlockRange = Range(match.range(at: 1), in: markdown)!
190+
return String(markdown[codeBlockRange])
191+
}
192+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
import OpenAIService
3+
4+
/// Quickly ask a question to ChatGPT.
5+
func askChatGPT(systemPrompt: String, question: String) async throws -> String {
6+
let service = ChatGPTService(systemPrompt: systemPrompt)
7+
return try await service.sendAndWait(content: question)
8+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Foundation
2+
import OpenAIService
3+
4+
/// This is a magic function that can do anything with no-code. See
5+
/// https://github.com/Torantulino/AI-Functions for more info.
6+
func callAIFunction(
7+
function: String,
8+
args: [Any?],
9+
description: String
10+
) async throws -> String {
11+
let args = args.map { arg -> String in
12+
if let arg = arg {
13+
return String(describing: arg)
14+
} else {
15+
return "None"
16+
}
17+
}
18+
let argsString = args.joined(separator: ", ")
19+
let service = ChatGPTService(
20+
systemPrompt: "You are now the following python function: ```# \(description)\n\(function)```\n\nOnly respond with your `return` value."
21+
)
22+
return try await service.sendAndWait(content: argsString)
23+
}

Core/Sources/ChatPlugins/ChatPlugin.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Foundation
22
import OpenAIService
33

4-
public protocol ChatPlugin {
4+
public protocol ChatPlugin: AnyObject {
55
/// Should be [a-zA-Z0-9]+
66
static var command: String { get }
77
var name: String { get }
88

9-
init(inside chatGPTService: ChatGPTServiceType, delegate: ChatPluginDelegate)
9+
init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate)
1010
func send(content: String) async
1111
func cancel() async
1212
func stopResponding() async
@@ -17,4 +17,5 @@ public protocol ChatPluginDelegate: AnyObject {
1717
func pluginDidEnd(_ plugin: ChatPlugin)
1818
func pluginDidStartResponding(_ plugin: ChatPlugin)
1919
func pluginDidEndResponding(_ plugin: ChatPlugin)
20+
func shouldStartAnotherPlugin(_ type: ChatPlugin.Type, withContent: String)
2021
}

Core/Sources/ChatPlugins/TerminalChatPlugin.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ public actor TerminalChatPlugin: ChatPlugin {
77
public static var command: String { "run" }
88
public nonisolated var name: String { "Terminal" }
99

10-
let chatGPTService: ChatGPTServiceType
10+
let chatGPTService: any ChatGPTServiceType
1111
var terminal: TerminalType = Terminal()
1212
var isCancelled = false
1313
weak var delegate: ChatPluginDelegate?
1414

15-
public init(inside chatGPTService: ChatGPTServiceType, delegate: ChatPluginDelegate) {
15+
public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) {
1616
self.chatGPTService = chatGPTService
1717
self.delegate = delegate
1818
}
@@ -43,9 +43,12 @@ public actor TerminalChatPlugin: ChatPlugin {
4343

4444
if isCancelled { throw CancellationError() }
4545

46+
let env = ProcessInfo.processInfo.environment
47+
let shell = env["SHELL"] ?? "/bin/bash"
48+
4649
let output = terminal.streamCommand(
47-
"/bin/bash",
48-
arguments: ["-c", content],
50+
shell,
51+
arguments: ["-l", "-c", content],
4952
currentDirectoryPath: projectURL?.path ?? fileURL.path,
5053
environment: [
5154
"PROJECT_ROOT": projectURL?.path ?? fileURL.path,

0 commit comments

Comments
 (0)