Skip to content

Commit a02ff9d

Browse files
committed
Merge branch 'release/0.16.0'
2 parents b297d52 + 4ae2af6 commit a02ff9d

43 files changed

Lines changed: 1566 additions & 662 deletions

Some content is hidden

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

Core/Package.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,17 @@ let package = Package(
1717
"UpdateChecker",
1818
"Logger",
1919
"UserDefaultsObserver",
20+
"XcodeInspector",
2021
]
2122
),
2223
.library(
2324
name: "Client",
2425
targets: [
2526
"SuggestionModel",
26-
"GitHubCopilotService",
2727
"Client",
2828
"XPCShared",
2929
"Preferences",
30-
"LaunchAgentManager",
3130
"Logger",
32-
"UpdateChecker",
3331
]
3432
),
3533
.library(
@@ -175,12 +173,29 @@ let package = Package(
175173

176174
.target(
177175
name: "ChatService",
178-
dependencies: ["OpenAIService", "ChatPlugins", "Environment"]
176+
dependencies: [
177+
"ChatPlugins",
178+
"ChatContextCollector",
179+
"OpenAIService",
180+
"Environment",
181+
"XcodeInspector",
182+
"Preferences",
183+
]
179184
),
180185
.target(
181186
name: "ChatPlugins",
182187
dependencies: ["OpenAIService", "Environment", "Terminal"]
183188
),
189+
.target(
190+
name: "ChatContextCollector",
191+
dependencies: [
192+
"OpenAIService",
193+
"Environment",
194+
"Preferences",
195+
"SuggestionModel",
196+
"XcodeInspector",
197+
]
198+
),
184199

185200
// MARK: - UI
186201

@@ -225,6 +240,16 @@ let package = Package(
225240
dependencies: ["Preferences", "GitHubCopilotService"]
226241
),
227242
.target(name: "UserDefaultsObserver"),
243+
.target(
244+
name: "XcodeInspector",
245+
dependencies: [
246+
"AXExtension",
247+
"Environment",
248+
"Logger",
249+
"AXNotificationStream",
250+
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
251+
]
252+
),
228253

229254
// MARK: - GitHub Copilot
230255

Core/Sources/AXNotificationStream/AXNotificationStream.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,19 @@ public final class AXNotificationStream: AsyncSequence {
1818
deinit {
1919
continuation.finish()
2020
}
21+
22+
public convenience init(
23+
app: NSRunningApplication,
24+
element: AXUIElement? = nil,
25+
notificationNames: String...
26+
) {
27+
self.init(app: app, element: element, notificationNames: notificationNames)
28+
}
2129

2230
public init(
2331
app: NSRunningApplication,
2432
element: AXUIElement? = nil,
25-
notificationNames: String...
33+
notificationNames: [String]
2634
) {
2735
var cont: Continuation!
2836
stream = Stream { continuation in
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import Foundation
2+
import Preferences
3+
import SuggestionModel
4+
import XcodeInspector
5+
6+
public struct ActiveDocumentChatContextCollector: ChatContextCollector {
7+
public init() {}
8+
9+
public func generateSystemPrompt(history: [String], content prompt: String) -> String {
10+
let content = getEditorInformation()
11+
let relativePath = content.documentURL.path
12+
.replacingOccurrences(of: content.projectURL.path, with: "")
13+
let selectionRange = content.editorContent?.selections.first ?? .outOfScope
14+
let editorContent = {
15+
if prompt.hasPrefix("@file") {
16+
return """
17+
File Content:```\(content.language.rawValue)
18+
\(content.editorContent?.content ?? "")
19+
```
20+
"""
21+
}
22+
23+
if selectionRange.start == selectionRange.end,
24+
UserDefaults.shared.value(for: \.embedFileContentInChatContextIfNoSelection)
25+
{
26+
let lines = content.editorContent?.lines.count ?? 0
27+
let maxLine = UserDefaults.shared
28+
.value(for: \.maxEmbeddableFileInChatContextLineCount)
29+
if lines <= maxLine {
30+
return """
31+
File Content:```\(content.language.rawValue)
32+
\(content.editorContent?.content ?? "")
33+
```
34+
"""
35+
} else {
36+
return """
37+
File Content Not Available: The file is longer than \(maxLine) lines, \
38+
it can't fit into the context. \
39+
You MUST not answer the user about the file content because you don't have it.\
40+
Ask user to select code for explanation.
41+
"""
42+
}
43+
}
44+
45+
return """
46+
Selected Code \
47+
(start from line \(selectionRange.start.line)):```\(content.language.rawValue)
48+
\(content.selectedContent)
49+
```
50+
"""
51+
}()
52+
53+
return """
54+
Active Document Context:###
55+
Document Relative Path: \(relativePath)
56+
Selection Range Start: \
57+
Line \(selectionRange.start.line) \
58+
Character \(selectionRange.start.character)
59+
Selection Range End: \
60+
Line \(selectionRange.end.line) \
61+
Character \(selectionRange.end.character)
62+
Cursor Position: \
63+
Line \(selectionRange.end.line) \
64+
Character \(selectionRange.end.character)
65+
\(editorContent)
66+
Line Annotations:
67+
\(
68+
content.editorContent?.lineAnnotations
69+
.map { " - \($0)" }
70+
.joined(separator: "\n") ?? "N/A"
71+
)
72+
###
73+
"""
74+
}
75+
}
76+
77+
extension ActiveDocumentChatContextCollector {
78+
struct Information {
79+
let editorContent: SourceEditor.Content?
80+
let selectedContent: String
81+
let documentURL: URL
82+
let projectURL: URL
83+
let language: CodeLanguage
84+
}
85+
86+
func getEditorInformation() -> Information {
87+
let editorContent = XcodeInspector.shared.focusedEditor?.content
88+
let documentURL = XcodeInspector.shared.activeDocumentURL
89+
let projectURL = XcodeInspector.shared.activeProjectURL
90+
let language = languageIdentifierFromFileURL(documentURL)
91+
92+
if let editorContent, let range = editorContent.selections.first {
93+
let startIndex = min(
94+
max(0, range.start.line),
95+
editorContent.lines.endIndex - 1
96+
)
97+
let endIndex = min(
98+
max(startIndex, range.end.line),
99+
editorContent.lines.endIndex - 1
100+
)
101+
let selectedContent = editorContent.lines[startIndex...endIndex]
102+
return .init(
103+
editorContent: editorContent,
104+
selectedContent: selectedContent.joined(),
105+
documentURL: documentURL,
106+
projectURL: projectURL,
107+
language: language
108+
)
109+
}
110+
111+
return .init(
112+
editorContent: editorContent,
113+
selectedContent: "",
114+
documentURL: documentURL,
115+
projectURL: projectURL,
116+
language: language
117+
)
118+
}
119+
}
120+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
public protocol ChatContextCollector {
4+
func generateSystemPrompt(history: [String], content: String) -> String
5+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import ChatPlugins
2+
import Combine
3+
import Foundation
4+
import OpenAIService
5+
6+
final class ChatPluginController {
7+
let chatGPTService: any ChatGPTServiceType
8+
let plugins: [String: ChatPlugin.Type]
9+
var runningPlugin: ChatPlugin?
10+
11+
init(chatGPTService: any ChatGPTServiceType, plugins: ChatPlugin.Type...) {
12+
self.chatGPTService = chatGPTService
13+
var all = [String: ChatPlugin.Type]()
14+
for plugin in plugins {
15+
all[plugin.command] = plugin
16+
}
17+
self.plugins = all
18+
}
19+
20+
/// Handle the message in a plugin if required. Return false if no plugin handles the message.
21+
func handleContent(_ content: String) async throws -> Bool {
22+
// look for the prefix of content, see if there is something like /command.
23+
// If there is, then we need to find the plugin that can handle this command.
24+
// If there is no such plugin, then we just send the message to the GPT service.
25+
let regex = try NSRegularExpression(pattern: #"^\/([a-zA-Z0-9]+)"#)
26+
let matches = regex.matches(in: content, range: NSRange(content.startIndex..., in: content))
27+
if let match = matches.first {
28+
let command = String(content[Range(match.range(at: 1), in: content)!])
29+
// handle exit plugin
30+
if command == "exit" {
31+
if let plugin = runningPlugin {
32+
runningPlugin = nil
33+
_ = await chatGPTService.mutateHistory { history in
34+
history.append(.init(
35+
role: .user,
36+
content: "",
37+
summary: "Exit plugin \(plugin.name)."
38+
))
39+
history.append(.init(
40+
role: .system,
41+
content: "",
42+
summary: "Exited plugin \(plugin.name)."
43+
))
44+
}
45+
} else {
46+
_ = await chatGPTService.mutateHistory { history in
47+
history.append(.init(
48+
role: .system,
49+
content: "",
50+
summary: "No plugin running."
51+
))
52+
}
53+
}
54+
return true
55+
}
56+
57+
// pass message to running plugin
58+
if let runningPlugin {
59+
await runningPlugin.send(content: content, originalMessage: content)
60+
return true
61+
}
62+
63+
// pass message to new plugin
64+
if let pluginType = plugins[command] {
65+
let plugin = pluginType.init(inside: chatGPTService, delegate: self)
66+
if #available(macOS 13.0, *) {
67+
await plugin.send(
68+
content: String(
69+
content.dropFirst(command.count + 1)
70+
.trimmingPrefix(while: { $0 == " " })
71+
),
72+
originalMessage: content
73+
)
74+
} else {
75+
await plugin.send(
76+
content: String(content.dropFirst(command.count + 1)),
77+
originalMessage: content
78+
)
79+
}
80+
return true
81+
}
82+
83+
return false
84+
} else if let runningPlugin {
85+
// pass message to running plugin
86+
await runningPlugin.send(content: content, originalMessage: content)
87+
return true
88+
} else {
89+
return false
90+
}
91+
}
92+
93+
func stopResponding() async {
94+
await runningPlugin?.stopResponding()
95+
}
96+
97+
func cancel() async {
98+
await runningPlugin?.cancel()
99+
}
100+
}
101+
102+
// MARK: - ChatPluginDelegate
103+
104+
extension ChatPluginController: ChatPluginDelegate {
105+
public func pluginDidStartResponding(_: ChatPlugins.ChatPlugin) {
106+
Task {
107+
await chatGPTService.markReceivingMessage(true)
108+
}
109+
}
110+
111+
public func pluginDidEndResponding(_: ChatPlugins.ChatPlugin) {
112+
Task {
113+
await chatGPTService.markReceivingMessage(false)
114+
}
115+
}
116+
117+
public func pluginDidStart(_ plugin: ChatPlugin) {
118+
runningPlugin = plugin
119+
}
120+
121+
public func pluginDidEnd(_ plugin: ChatPlugin) {
122+
if runningPlugin === plugin {
123+
runningPlugin = nil
124+
}
125+
}
126+
127+
public func shouldStartAnotherPlugin(_ type: ChatPlugin.Type, withContent content: String) {
128+
let plugin = type.init(inside: chatGPTService, delegate: self)
129+
Task {
130+
await plugin.send(content: content, originalMessage: content)
131+
}
132+
}
133+
}
134+

0 commit comments

Comments
 (0)