forked from intitni/CopilotForXcode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTerminalChatPlugin.swift
More file actions
165 lines (138 loc) · 5.46 KB
/
TerminalChatPlugin.swift
File metadata and controls
165 lines (138 loc) · 5.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import ChatBasic
import Foundation
import Terminal
import XcodeInspector
public final class TerminalChatPlugin: ChatPlugin {
public static var id: String { "com.intii.terminal" }
public static var command: String { "shell" }
public static var name: String { "Shell" }
public static var description: String { """
Run the command in the message from shell.
You can use environment variable `$FILE_PATH` and `$PROJECT_ROOT` to access the current file path and project root.
""" }
let terminal: TerminalType
init(terminal: TerminalType) {
self.terminal = terminal
}
public init() {
terminal = Terminal()
}
public func getTextContent(from request: Request) async
-> AsyncStream<String>
{
return .init { continuation in
let task = Task {
do {
let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL
let projectURL = XcodeInspector.shared.realtimeActiveProjectURL
var environment = [String: String]()
if let fileURL {
environment["FILE_PATH"] = fileURL.path
}
if let projectURL {
environment["PROJECT_ROOT"] = projectURL.path
}
try Task.checkCancellation()
let env = ProcessInfo.processInfo.environment
let shell = env["SHELL"] ?? "/bin/bash"
let output = terminal.streamCommand(
shell,
arguments: ["-i", "-l", "-c", request.text],
currentDirectoryURL: projectURL,
environment: environment
)
var accumulatedOutput = ""
for try await content in output {
try Task.checkCancellation()
accumulatedOutput += content
continuation.yield(accumulatedOutput)
}
} catch let error as Terminal.TerminationError {
let errorMessage = "\n\n[error: \(error.reason)]"
continuation.yield(errorMessage)
} catch {
let errorMessage = "\n\n[error: \(error.localizedDescription)]"
continuation.yield(errorMessage)
}
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
Task {
await self.terminal.terminate()
}
}
}
}
public func sendForTextResponse(_ request: Request) async
-> AsyncThrowingStream<String, any Error>
{
let stream = await getTextContent(from: request)
return .init { continuation in
let task = Task {
continuation.yield("Executing command: `\(request.text)`\n\n")
continuation.yield("```console\n")
for await text in stream {
try Task.checkCancellation()
continuation.yield(text)
}
continuation.yield("\n```\n")
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
public func formatContent(_ content: Response.Content) -> Response.Content {
switch content {
case let .text(content):
return .text("""
```console
\(content)
```
""")
}
}
public func sendForComplicatedResponse(_ request: Request) async
-> AsyncThrowingStream<Response, any Error>
{
return .init { continuation in
let task = Task {
var updateTime = Date()
continuation.yield(.startAction(id: "run", task: "Run `\(request.text)`"))
let textStream = await getTextContent(from: request)
var previousOutput = ""
continuation.yield(.finishAction(
id: "run",
result: .success("Executed.")
))
for await accumulatedOutput in textStream {
try Task.checkCancellation()
let newContent = accumulatedOutput.dropFirst(previousOutput.count)
previousOutput = accumulatedOutput
if !newContent.isEmpty {
if Date().timeIntervalSince(updateTime) > 60 * 2 {
continuation.yield(.startNewMessage)
continuation.yield(.startAction(
id: "run",
task: "Continue `\(request.text)`"
))
continuation.yield(.finishAction(
id: "run",
result: .success("Executed.")
))
continuation.yield(.content(.text("[continue]\n")))
updateTime = Date()
}
continuation.yield(.content(.text(String(newContent))))
}
}
continuation.finish()
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
}