Skip to content

Commit 84244c0

Browse files
committed
Reimplemnt plugins with ChatPlugin
1 parent a59fb49 commit 84244c0

File tree

21 files changed

+494
-675
lines changed

21 files changed

+494
-675
lines changed

ChatPlugins/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1600"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "ChatPlugins"
19+
BuildableName = "ChatPlugins"
20+
BlueprintName = "ChatPlugins"
21+
ReferencedContainer = "container:">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
32+
</TestAction>
33+
<LaunchAction
34+
buildConfiguration = "Debug"
35+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
36+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
37+
launchStyle = "0"
38+
useCustomWorkingDirectory = "NO"
39+
ignoresPersistentStateOnLaunch = "NO"
40+
debugDocumentVersioning = "YES"
41+
debugServiceExtension = "internal"
42+
allowLocationSimulation = "YES">
43+
</LaunchAction>
44+
<ProfileAction
45+
buildConfiguration = "Release"
46+
shouldUseLaunchSchemeArgsEnv = "YES"
47+
savedToolIdentifier = ""
48+
useCustomWorkingDirectory = "NO"
49+
debugDocumentVersioning = "YES">
50+
<MacroExpansion>
51+
<BuildableReference
52+
BuildableIdentifier = "primary"
53+
BlueprintIdentifier = "ChatPlugins"
54+
BuildableName = "ChatPlugins"
55+
BlueprintName = "ChatPlugins"
56+
ReferencedContainer = "container:">
57+
</BuildableReference>
58+
</MacroExpansion>
59+
</ProfileAction>
60+
<AnalyzeAction
61+
buildConfiguration = "Debug">
62+
</AnalyzeAction>
63+
<ArchiveAction
64+
buildConfiguration = "Release"
65+
revealArchiveInOrganizer = "YES">
66+
</ArchiveAction>
67+
</Scheme>

ChatPlugins/Package.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// swift-tools-version: 5.8
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "ChatPlugins",
8+
platforms: [.macOS(.v12)],
9+
products: [
10+
.library(
11+
name: "ChatPlugins",
12+
targets: ["TerminalChatPlugin", "ShortcutChatPlugin"]
13+
),
14+
],
15+
dependencies: [
16+
.package(path: "../Tool"),
17+
],
18+
targets: [
19+
.target(
20+
name: "TerminalChatPlugin",
21+
dependencies: [
22+
.product(name: "Chat", package: "Tool"),
23+
.product(name: "Terminal", package: "Tool"),
24+
.product(name: "AppMonitoring", package: "Tool"),
25+
]
26+
),
27+
.target(
28+
name: "ShortcutChatPlugin",
29+
dependencies: [
30+
.product(name: "Chat", package: "Tool"),
31+
.product(name: "Terminal", package: "Tool"),
32+
.product(name: "AppMonitoring", package: "Tool"),
33+
]
34+
),
35+
]
36+
)
37+
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import ChatBasic
2+
import Foundation
3+
import Terminal
4+
5+
public final class ShortcutChatPlugin: ChatPlugin {
6+
public static var id: String { "com.intii.shortcut" }
7+
public static var command: String { "shortcut" }
8+
public static var name: String { "Shortcut" }
9+
10+
let terminal: TerminalType
11+
12+
init(terminal: TerminalType) {
13+
self.terminal = terminal
14+
}
15+
16+
public init() {
17+
terminal = Terminal()
18+
}
19+
20+
public func send(_ request: Request) async -> AsyncThrowingStream<Response, any Error> {
21+
return .init { continuation in
22+
let task = Task {
23+
let id = "\(Self.command)-\(UUID().uuidString)"
24+
25+
guard let shortcutName = request.arguments.first, !shortcutName.isEmpty else {
26+
continuation.yield(.content(.text(
27+
"Please provide the shortcut name in format: `/\(Self.command)(shortcut name)`"
28+
)))
29+
return
30+
}
31+
32+
var input = String(request.text).trimmingCharacters(in: .whitespacesAndNewlines)
33+
if input.isEmpty {
34+
// if no input detected, use the previous message as input
35+
input = request.history.last?.content ?? ""
36+
}
37+
38+
do {
39+
continuation.yield(.startAction(
40+
id: "run",
41+
task: "Run shortcut `\(shortcutName)`"
42+
))
43+
44+
let env = ProcessInfo.processInfo.environment
45+
let shell = env["SHELL"] ?? "/bin/bash"
46+
let temporaryURL = FileManager.default.temporaryDirectory
47+
let temporaryInputFileURL = temporaryURL
48+
.appendingPathComponent("\(id)-input.txt")
49+
let temporaryOutputFileURL = temporaryURL
50+
.appendingPathComponent("\(id)-output")
51+
52+
try input.write(to: temporaryInputFileURL, atomically: true, encoding: .utf8)
53+
54+
let command = """
55+
shortcuts run "\(shortcutName)" \
56+
-i "\(temporaryInputFileURL.path)" \
57+
-o "\(temporaryOutputFileURL.path)"
58+
"""
59+
60+
continuation.yield(.startAction(
61+
id: "run",
62+
task: "Run shortcut \(shortcutName)"
63+
))
64+
65+
let result = try await terminal.runCommand(
66+
shell,
67+
arguments: ["-i", "-l", "-c", command],
68+
currentDirectoryURL: nil,
69+
environment: [:]
70+
)
71+
72+
continuation.yield(.finishAction(id: "run", result: result))
73+
74+
await Task.yield()
75+
try Task.checkCancellation()
76+
77+
if FileManager.default.fileExists(atPath: temporaryOutputFileURL.path) {
78+
let data = try Data(contentsOf: temporaryOutputFileURL)
79+
if let text = String(data: data, encoding: .utf8) {
80+
var response = text
81+
if response.isEmpty {
82+
response = "Finished"
83+
}
84+
continuation.yield(.content(.text(response)))
85+
} else {
86+
let content = """
87+
[View File](\(temporaryOutputFileURL))
88+
"""
89+
continuation.yield(.content(.text(content)))
90+
}
91+
} else {
92+
continuation.yield(.content(.text("Finished")))
93+
}
94+
95+
} catch {
96+
continuation.yield(.content(.text(error.localizedDescription)))
97+
}
98+
99+
continuation.finish()
100+
}
101+
102+
continuation.onTermination = { _ in
103+
task.cancel()
104+
}
105+
}
106+
}
107+
}
108+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import ChatBasic
2+
import Foundation
3+
import Terminal
4+
import XcodeInspector
5+
6+
public final class TerminalChatPlugin: ChatPlugin {
7+
public static var id: String { "com.intii.terminal" }
8+
public static var command: String { "run" }
9+
public static var name: String { "Terminal" }
10+
11+
let terminal: TerminalType
12+
13+
init(terminal: TerminalType) {
14+
self.terminal = terminal
15+
}
16+
17+
public init() {
18+
terminal = Terminal()
19+
}
20+
21+
public func formatContent(_ content: Response.Content) -> Response.Content {
22+
switch content {
23+
case let .text(content):
24+
return .text("""
25+
```sh
26+
\(content)
27+
```
28+
""")
29+
}
30+
}
31+
32+
public func send(_ request: Request) async -> AsyncThrowingStream<Response, any Error> {
33+
return .init { continuation in
34+
let task = Task {
35+
var updateTime = Date()
36+
37+
func streamOutput(_ content: String) {
38+
defer { updateTime = Date() }
39+
if Date().timeIntervalSince(updateTime) > 60 * 2 {
40+
continuation.yield(.startNewMessage)
41+
continuation.yield(.startAction(
42+
id: "run",
43+
task: "Continue `\(request.text)`"
44+
))
45+
continuation.yield(.finishAction(id: "run", result: "Printing output..."))
46+
continuation.yield(.content(.text("[continue]\n")))
47+
continuation.yield(.content(.text(content)))
48+
} else {
49+
continuation.yield(.content(.text(content)))
50+
}
51+
}
52+
53+
do {
54+
let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL
55+
let projectURL = await XcodeInspector.shared.safe.realtimeActiveProjectURL
56+
57+
var environment = [String: String]()
58+
if let fileURL {
59+
environment["FILE_PATH"] = fileURL.path
60+
}
61+
if let projectURL {
62+
environment["PROJECT_ROOT"] = projectURL.path
63+
}
64+
65+
try Task.checkCancellation()
66+
67+
let env = ProcessInfo.processInfo.environment
68+
let shell = env["SHELL"] ?? "/bin/bash"
69+
70+
continuation.yield(.startAction(id: "run", task: "Run `\(request.text)`"))
71+
72+
let output = terminal.streamCommand(
73+
shell,
74+
arguments: ["-i", "-l", "-c", request.text],
75+
currentDirectoryURL: projectURL,
76+
environment: environment
77+
)
78+
79+
continuation.yield(.finishAction(id: "run", result: "Printing output..."))
80+
81+
for try await content in output {
82+
try Task.checkCancellation()
83+
streamOutput(content)
84+
}
85+
} catch let error as Terminal.TerminationError {
86+
continuation.yield(.content(.text("""
87+
88+
[error: \(error.reason)]
89+
""")))
90+
} catch {
91+
continuation.yield(.content(.text("""
92+
93+
[error: \(error.localizedDescription)]
94+
""")))
95+
}
96+
97+
continuation.finish()
98+
}
99+
100+
continuation.onTermination = { _ in
101+
task.cancel()
102+
Task {
103+
await self.terminal.terminate()
104+
}
105+
}
106+
}
107+
}
108+
}
109+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Testing
2+
@testable import ChatPlugins
3+
4+
@Test func example() async throws {
5+
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
6+
}

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@
205205
C8216B772980370100AD38C7 /* ReloadLaunchAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadLaunchAgent.swift; sourceTree = "<group>"; };
206206
C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = ExtensionPoint.appextensionpoint; sourceTree = "<group>"; };
207207
C82E38492A1F025F00D4EADF /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
208+
C84FD9D72CC671C600BE5093 /* ChatPlugins */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ChatPlugins; sourceTree = "<group>"; };
208209
C8520300293C4D9000460097 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
209210
C8520308293D805800460097 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
210211
C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptToCodeCommand.swift; sourceTree = "<group>"; };
@@ -341,6 +342,7 @@
341342
C81458AE293A009800135263 /* Config.debug.xcconfig */,
342343
C8CD828229B88006008D044D /* TestPlan.xctestplan */,
343344
C828B27D2B1F241500E7612A /* ExtensionPoint.appextensionpoint */,
345+
C84FD9D72CC671C600BE5093 /* ChatPlugins */,
344346
C81D181E2A1B509B006C1B70 /* Tool */,
345347
C8189B282938979000C9DCDA /* Core */,
346348
C8189B182938972F00C9DCDA /* Copilot for Xcode */,

0 commit comments

Comments
 (0)