Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion ChatPlugins/Sources/ShortcutChatPlugin/ShortcutChatPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,36 @@ public final class ShortcutChatPlugin: ChatPlugin {
terminal = Terminal()
}

public func send(_ request: Request) async -> AsyncThrowingStream<Response, any Error> {
public func sendForTextResponse(_ request: Request) async
-> AsyncThrowingStream<String, any Error>
{
let stream = await sendForComplicatedResponse(request)
return .init { continuation in
let task = Task {
do {
for try await response in stream {
switch response {
case let .content(.text(content)):
continuation.yield(content)
default:
break
}
}
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}

continuation.onTermination = { _ in
task.cancel()
}
}
}

public func sendForComplicatedResponse(_ request: Request) async
-> AsyncThrowingStream<Response, any Error>
{
return .init { continuation in
let task = Task {
let id = "\(Self.command)-\(UUID().uuidString)"
Expand Down
145 changes: 95 additions & 50 deletions ChatPlugins/Sources/TerminalChatPlugin/TerminalChatPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public final class TerminalChatPlugin: ChatPlugin {
public static var name: String { "Terminal" }
public static var description: String { """
Run the command in the message from terminal.

You can use environment variable `$FILE_PATH` and `$PROJECT_ROOT` to access the current file path and project root.
""" }

Expand All @@ -23,41 +23,11 @@ public final class TerminalChatPlugin: ChatPlugin {
terminal = Terminal()
}

public func formatContent(_ content: Response.Content) -> Response.Content {
switch content {
case let .text(content):
return .text("""
```sh
\(content)
```
""")
}
}

public func send(_ request: Request) async -> AsyncThrowingStream<Response, any Error> {
public func getTextContent(from request: Request) async
-> AsyncStream<String>
{
return .init { continuation in
let task = Task {
var updateTime = Date()

func streamOutput(_ content: String) {
defer { updateTime = Date() }
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")))
continuation.yield(.content(.text(content)))
} else {
continuation.yield(.content(.text(content)))
}
}

do {
let fileURL = XcodeInspector.shared.realtimeActiveDocumentURL
let projectURL = XcodeInspector.shared.realtimeActiveProjectURL
Expand All @@ -75,34 +45,25 @@ public final class TerminalChatPlugin: ChatPlugin {
let env = ProcessInfo.processInfo.environment
let shell = env["SHELL"] ?? "/bin/bash"

continuation.yield(.startAction(id: "run", task: "Run `\(request.text)`"))

let output = terminal.streamCommand(
shell,
arguments: ["-i", "-l", "-c", request.text],
currentDirectoryURL: projectURL,
environment: environment
)

continuation.yield(.finishAction(
id: "run",
result: .success("Executed.")
))

var accumulatedOutput = ""
for try await content in output {
try Task.checkCancellation()
streamOutput(content)
accumulatedOutput += content
continuation.yield(accumulatedOutput)
}
} catch let error as Terminal.TerminationError {
continuation.yield(.content(.text("""

[error: \(error.reason)]
""")))
let errorMessage = "\n\n[error: \(error.reason)]"
continuation.yield(errorMessage)
} catch {
continuation.yield(.content(.text("""

[error: \(error.localizedDescription)]
""")))
let errorMessage = "\n\n[error: \(error.localizedDescription)]"
continuation.yield(errorMessage)
}

continuation.finish()
Expand All @@ -116,5 +77,89 @@ public final class TerminalChatPlugin: ChatPlugin {
}
}
}

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()
}
}
}
}

6 changes: 3 additions & 3 deletions Core/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ let package = Package(
.product(name: "SuggestionBasic", package: "Tool"),
.product(name: "Logger", package: "Tool"),
.product(name: "Preferences", package: "Tool"),
].pro([
"ProClient",
].proCore([
"LicenseManagement",
])
),
.target(
Expand Down Expand Up @@ -348,7 +348,7 @@ extension [Target.Dependency] {
extension [Package.Dependency] {
var pro: [Package.Dependency] {
if isProIncluded {
return self + [.package(path: "../../Pro")]
return self + [.package(path: "../../Pro"), .package(path: "../../Pro/ProCore")]
}
return self
}
Expand Down
5 changes: 4 additions & 1 deletion Core/Sources/ChatGPTChatTab/Views/ThemedMarkdownText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ extension MarkdownUI.Theme {
}
.codeBlock { configuration in
let wrapCode = UserDefaults.shared.value(for: \.wrapCodeInChatCodeBlock)
|| ["plaintext", "text", "markdown", "sh", "bash", "shell", "latex", "tex"]
|| [
"plaintext", "text", "markdown", "sh", "console", "bash", "shell", "latex",
"tex"
]
.contains(configuration.language)

if wrapCode {
Expand Down
2 changes: 1 addition & 1 deletion Core/Sources/ChatService/AllPlugins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ final class LegacyChatPluginWrapper<Plugin: ChatPlugin>: LegacyChatPlugin {

let plugin = Plugin()

let stream = await plugin.send(.init(
let stream = await plugin.sendForComplicatedResponse(.init(
text: content,
arguments: [],
history: chatGPTService.memory.history
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ struct ChatModelEdit {
var openAICompatibleSupportsMultipartMessageContent = true
var requiresBeginWithUserMessage = false
var customBody: String = ""
var supportsImages: Bool = true
}

enum Action: Equatable, BindableAction {
Expand Down Expand Up @@ -290,7 +291,9 @@ extension ChatModel {
return state.supportsFunctionCalling
}
}(),
modelName: state.modelName.trimmingCharacters(in: .whitespacesAndNewlines),
supportsImage: state.supportsImages,
modelName: state.modelName
.trimmingCharacters(in: .whitespacesAndNewlines),
openAIInfo: .init(
organizationID: state.openAIOrganizationID,
projectID: state.openAIProjectID
Expand Down Expand Up @@ -331,7 +334,8 @@ extension ChatModel {
openAICompatibleSupportsMultipartMessageContent: info.openAICompatibleInfo
.supportsMultipartMessageContent,
requiresBeginWithUserMessage: info.openAICompatibleInfo.requiresBeginWithUserMessage,
customBody: info.customBodyInfo.jsonBody
customBody: info.customBodyInfo.jsonBody,
supportsImages: info.supportsImage
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ struct ChatModelEditView: View {
TextField(text: $store.openAIProjectID, prompt: Text("Optional")) {
Text("Project ID")
}

Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}

VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
Expand Down Expand Up @@ -386,6 +390,10 @@ struct ChatModelEditView: View {

MaxTokensTextField(store: store)
SupportsFunctionCallingToggle(store: store)

Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
}
}
}
Expand Down Expand Up @@ -435,6 +443,10 @@ struct ChatModelEditView: View {
Toggle(isOn: $store.requiresBeginWithUserMessage) {
Text("Requires the first message to be from the user")
}

Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
}
}
}
Expand Down Expand Up @@ -473,6 +485,10 @@ struct ChatModelEditView: View {
MaxTokensTextField(store: store)

TextField("API Version", text: $store.apiVersion, prompt: Text("v1"))

Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}
}
}
}
Expand All @@ -496,6 +512,10 @@ struct ChatModelEditView: View {
Text("Keep Alive")
}

Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}

VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" For more details, please visit [https://ollama.com](https://ollama.com)."
Expand Down Expand Up @@ -539,6 +559,10 @@ struct ChatModelEditView: View {
}

MaxTokensTextField(store: store)

Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}

VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
Expand Down Expand Up @@ -572,6 +596,10 @@ struct ChatModelEditView: View {
Toggle(isOn: $store.openAICompatibleSupportsMultipartMessageContent) {
Text("Support multi-part message content")
}

Toggle(isOn: $store.supportsImages) {
Text("Supports Images")
}

VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ struct SuggestionFeatureDisabledLanguageListView: View {
if settings.suggestionFeatureDisabledLanguageList.isEmpty {
Text("""
Empty
Disable the language of a file by right clicking the circular widget.
Disable the language of a file by right clicking the indicator widget.
""")
.multilineTextAlignment(.center)
.padding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ struct SuggestionFeatureEnabledProjectListView: View {
Text("""
Empty
Add project with "+" button
Or right clicking the circular widget
Or right clicking the indicator widget
""")
.multilineTextAlignment(.center)
}
Expand Down
Loading