Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import ChatPlugin
import Environment
import Foundation
import OpenAIService
import Parsing
import Terminal

public actor ShortcutInputChatPlugin: ChatPlugin {
public static var command: String { "shortcutInput" }
public nonisolated var name: String { "Shortcut Input" }

let chatGPTService: any ChatGPTServiceType
var terminal: TerminalType = Terminal()
var isCancelled = false
weak var delegate: ChatPluginDelegate?

public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) {
self.chatGPTService = chatGPTService
self.delegate = delegate
}

public func send(content: String, originalMessage: String) async {
delegate?.pluginDidStart(self)
delegate?.pluginDidStartResponding(self)

defer {
delegate?.pluginDidEndResponding(self)
delegate?.pluginDidEnd(self)
}

let id = "\(Self.command)-\(UUID().uuidString)"

var content = content[...]
let firstParenthesisParser = PrefixThrough("(")
let shortcutNameParser = PrefixUpTo(")")

_ = try? firstParenthesisParser.parse(&content)
let shortcutName = try? shortcutNameParser.parse(&content)
_ = try? PrefixThrough(")").parse(&content)

guard let shortcutName, !shortcutName.isEmpty else {
let id = "\(Self.command)-\(UUID().uuidString)"
let reply = ChatMessage(
id: id,
role: .assistant,
content: "Please provide the shortcut name in format: `/\(Self.command)(shortcut name)`."
)
await chatGPTService.mutateHistory { history in
history.append(reply)
}
return
}

var input = String(content).trimmingCharacters(in: .whitespacesAndNewlines)
if input.isEmpty {
// if no input detected, use the previous message as input
input = await chatGPTService.history.last?.content ?? ""
}

do {
if isCancelled { throw CancellationError() }

let env = ProcessInfo.processInfo.environment
let shell = env["SHELL"] ?? "/bin/bash"
let temporaryURL = FileManager.default.temporaryDirectory
let temporaryInputFileURL = temporaryURL
.appendingPathComponent("\(id)-input.txt")
let temporaryOutputFileURL = temporaryURL
.appendingPathComponent("\(id)-output")

try input.write(to: temporaryInputFileURL, atomically: true, encoding: .utf8)

let command = """
shortcuts run "\(shortcutName)" \
-i "\(temporaryInputFileURL.path)" \
-o "\(temporaryOutputFileURL.path)"
"""

_ = try await terminal.runCommand(
shell,
arguments: ["-i", "-l", "-c", command],
currentDirectoryPath: "/",
environment: [:]
)

await Task.yield()

if FileManager.default.fileExists(atPath: temporaryOutputFileURL.path) {
let data = try Data(contentsOf: temporaryOutputFileURL)
if let text = String(data: data, encoding: .utf8) {
if text.isEmpty { return }
let stream = try await chatGPTService.send(content: text, summary: nil)
for try await _ in stream {}
} else {
let text = """
[View File](\(temporaryOutputFileURL))
"""
let stream = try await chatGPTService.send(content: text, summary: nil)
for try await _ in stream {}
}

return
}
} catch {
let id = "\(Self.command)-\(UUID().uuidString)"
let reply = ChatMessage(
id: id,
role: .assistant,
content: error.localizedDescription
)
await chatGPTService.mutateHistory { history in
history.append(reply)
}
}
}

public func cancel() async {
isCancelled = true
await terminal.terminate()
}

public func stopResponding() async {
isCancelled = true
await terminal.terminate()
}
}

1 change: 1 addition & 0 deletions Core/Sources/ChatService/AllPlugins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ let allPlugins: [ChatPlugin.Type] = [
MathChatPlugin.self,
SearchChatPlugin.self,
ShortcutChatPlugin.self,
ShortcutInputChatPlugin.self,
]

9 changes: 3 additions & 6 deletions Core/Sources/ChatService/ChatPluginController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ final class ChatPluginController {
let chatGPTService: any ChatGPTServiceType
let plugins: [String: ChatPlugin.Type]
var runningPlugin: ChatPlugin?
weak var chatService: ChatService?

init(chatGPTService: any ChatGPTServiceType, plugins: [ChatPlugin.Type]) {
self.chatGPTService = chatGPTService
Expand Down Expand Up @@ -107,15 +108,11 @@ final class ChatPluginController {

extension ChatPluginController: ChatPluginDelegate {
public func pluginDidStartResponding(_: ChatPlugin) {
Task {
await chatGPTService.markReceivingMessage(true)
}
chatService?.isReceivingMessage = true
}

public func pluginDidEndResponding(_: ChatPlugin) {
Task {
await chatGPTService.markReceivingMessage(false)
}
chatService?.isReceivingMessage = false
}

public func pluginDidStart(_ plugin: ChatPlugin) {
Expand Down
11 changes: 10 additions & 1 deletion Core/Sources/ChatService/ChatService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import OpenAIService

public final class ChatService: ObservableObject {
public let chatGPTService: any ChatGPTServiceType
public var allPluginCommands: [String] { allPlugins.map { $0.command } }
let pluginController: ChatPluginController
let contextController: DynamicContextController
var cancellable = Set<AnyCancellable>()
@Published public internal(set) var isReceivingMessage = false
@Published public internal(set) var systemPrompt = UserDefaults.shared
.value(for: \.defaultChatSystemPrompt)
@Published public internal(set) var extraSystemPrompt = ""
Expand All @@ -21,30 +23,37 @@ public final class ChatService: ObservableObject {
contextCollectors: ActiveDocumentChatContextCollector()
)

pluginController.chatService = self
chatGPTService.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}.store(in: &cancellable)
}

public func send(content: String) async throws {
guard !isReceivingMessage else { throw CancellationError() }
let handledInPlugin = try await pluginController.handleContent(content)
if handledInPlugin { return }
try await contextController.updatePromptToMatchContent(systemPrompt: """
\(systemPrompt)
\(extraSystemPrompt)
""", content: content)

_ = try await chatGPTService.send(content: content, summary: nil)
let stream = try await chatGPTService.send(content: content, summary: nil)
isReceivingMessage = true
for try await _ in stream {}
isReceivingMessage = false
}

public func stopReceivingMessage() async {
await pluginController.stopResponding()
await chatGPTService.stopReceivingMessage()
isReceivingMessage = false
}

public func clearHistory() async {
await pluginController.cancel()
await chatGPTService.clearHistory()
isReceivingMessage = false
}

public func resetPrompt() async {
Expand Down
4 changes: 2 additions & 2 deletions Core/Sources/Service/GUI/ChatProvider+Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ extension ChatProvider {
onCloseChat: @escaping () -> Void,
onSwitchContext: @escaping () -> Void
) {
self.init()
self.init(pluginIdentifiers: service.allPluginCommands)

let cancellable = service.objectWillChange.sink { [weak self] in
guard let self else { return }
Expand All @@ -23,7 +23,7 @@ extension ChatProvider {
text: message.summary ?? message.content
)
}
self.isReceivingMessage = await service.chatGPTService.isReceivingMessage
self.isReceivingMessage = service.isReceivingMessage
self.systemPrompt = service.systemPrompt
self.extraSystemPrompt = service.extraSystemPrompt
}
Expand Down
4 changes: 4 additions & 0 deletions Core/Sources/SuggestionWidget/ChatProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public final class ChatProvider: ObservableObject {
let id = UUID()
@Published public var history: [ChatMessage] = []
@Published public var isReceivingMessage = false
public var pluginIdentifiers: [String] = []
public var systemPrompt = ""
public var extraSystemPrompt = ""
public var onMessageSend: (String) -> Void
Expand All @@ -23,6 +24,7 @@ public final class ChatProvider: ObservableObject {
public init(
history: [ChatMessage] = [],
isReceivingMessage: Bool = false,
pluginIdentifiers: [String] = [],
onMessageSend: @escaping (String) -> Void = { _ in },
onStop: @escaping () -> Void = {},
onClear: @escaping () -> Void = {},
Expand All @@ -36,6 +38,7 @@ public final class ChatProvider: ObservableObject {
) {
self.history = history
self.isReceivingMessage = isReceivingMessage
self.pluginIdentifiers = pluginIdentifiers
self.onMessageSend = onMessageSend
self.onStop = onStop
self.onClear = onClear
Expand All @@ -59,6 +62,7 @@ public final class ChatProvider: ObservableObject {
public func triggerCustomCommand(_ command: CustomCommand) {
onRunCustomCommand(command)
}

public func setAsExtraPrompt(id: MessageID) { onSetAsExtraPrompt(id) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,29 +355,7 @@ struct ChatPanelInputArea: View {
text: $typedMessage,
font: .systemFont(ofSize: 14),
onSubmit: { submitText() },
completions: { text, _, range in
if text.isEmpty { return [] }
let availableFeatures = [
"/run",
"/airun",
"/math",
"/search",
"/shortcut",
"/exit",
"@selection",
"@file",
]
return availableFeatures
.filter { $0.hasPrefix(text) && $0 != text }
.compactMap {
guard let index = $0.index(
$0.startIndex,
offsetBy: range.location,
limitedBy: $0.endIndex
) else { return nil }
return String($0[index...])
}
}
completions: chatAutoCompletion
)
.padding(.top, 1)
.padding(.bottom, -1)
Expand Down Expand Up @@ -420,6 +398,28 @@ struct ChatPanelInputArea: View {
chat.send(typedMessage)
typedMessage = ""
}

func chatAutoCompletion(text: String, proposed: [String], range: NSRange) -> [String] {
guard text.count == 1 else { return [] }
let plugins = chat.pluginIdentifiers.map { "/\($0)" }
let availableFeatures = plugins + [
"/exit",
"@selection",
"@file",
]

let result: [String] = availableFeatures
.filter { $0.hasPrefix(text) && $0 != text }
.compactMap {
guard let index = $0.index(
$0.startIndex,
offsetBy: range.location,
limitedBy: $0.endIndex
) else { return nil }
return String($0[index...])
}
return result
}
}

struct ChatContextMenu: View {
Expand Down Expand Up @@ -700,3 +700,6 @@ struct ChatPanel_Light_Preview: PreviewProvider {
}
}




Loading