Skip to content

Commit f2c9cdd

Browse files
committed
Merge branch 'feature/chat-menu' into develop
2 parents 7e15e1e + 61bc6bc commit f2c9cdd

6 files changed

Lines changed: 116 additions & 83 deletions

File tree

Core/Sources/ChatService/ChatService.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ public final class ChatService: ObservableObject {
2020
let pluginController: ChatPluginController
2121
let contextController: DynamicContextController
2222
var cancellable = Set<AnyCancellable>()
23-
var systemPrompt = defaultSystemPrompt
24-
var extraSystemPrompt = ""
23+
@Published public internal(set) var systemPrompt = defaultSystemPrompt
24+
@Published public internal(set) var extraSystemPrompt = ""
2525

2626
public init<T: ChatGPTServiceType>(chatGPTService: T) {
2727
self.chatGPTService = chatGPTService
@@ -61,6 +61,11 @@ public final class ChatService: ObservableObject {
6161
await pluginController.cancel()
6262
await chatGPTService.clearHistory()
6363
}
64+
65+
public func resetPrompt() async {
66+
systemPrompt = defaultSystemPrompt
67+
extraSystemPrompt = ""
68+
}
6469

6570
public func deleteMessage(id: String) async {
6671
await chatGPTService.mutateHistory { messages in

Core/Sources/Service/GUI/ChatProvider+Service.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ChatService
2+
import Combine
23
import Foundation
34
import OpenAIService
45
import SuggestionWidget
@@ -11,6 +12,7 @@ extension ChatProvider {
1112
onSwitchContext: @escaping () -> Void
1213
) {
1314
self.init()
15+
1416
let cancellable = service.objectWillChange.sink { [weak self] in
1517
guard let self else { return }
1618
Task { @MainActor in
@@ -22,6 +24,8 @@ extension ChatProvider {
2224
)
2325
}
2426
self.isReceivingMessage = await service.chatGPTService.isReceivingMessage
27+
self.systemPrompt = service.systemPrompt
28+
self.extraSystemPrompt = service.extraSystemPrompt
2529
}
2630
}
2731

@@ -75,5 +79,19 @@ extension ChatProvider {
7579
}
7680
}
7781
}
82+
83+
onResetPrompt = {
84+
Task {
85+
await service.resetPrompt()
86+
}
87+
}
88+
89+
onRunCustomCommand = { command in
90+
Task {
91+
let commandHandler = PseudoCommandHandler()
92+
await commandHandler.handleCustomCommand(command)
93+
}
94+
}
7895
}
7996
}
97+

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ struct PseudoCommandHandler {
7474
return it
7575
}
7676
switch command.feature {
77-
case .customChat:
77+
// editor content is not required.
78+
case .customChat, .chatWithSelection:
7879
return .init(
7980
content: "",
8081
lines: [],
@@ -85,7 +86,8 @@ struct PseudoCommandHandler {
8586
indentSize: 0,
8687
usesTabsForIndentation: false
8788
)
88-
case .chatWithSelection, .promptToCode:
89+
// editor content is required.
90+
case .promptToCode:
8991
return nil
9092
}
9193
}() else {

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 3 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
234234
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent? {
235235
Task {
236236
do {
237-
try await startChatWithSelection(
238-
editor: editor,
237+
try await startChat(
239238
specifiedSystemPrompt: nil,
240239
extraSystemPrompt: nil,
241240
sendingMessageImmediately: nil,
@@ -289,8 +288,7 @@ extension WindowBaseCommandHandler {
289288

290289
switch command.feature {
291290
case let .chatWithSelection(extraSystemPrompt, prompt):
292-
try await startChatWithSelection(
293-
editor: editor,
291+
try await startChat(
294292
specifiedSystemPrompt: nil,
295293
extraSystemPrompt: extraSystemPrompt,
296294
sendingMessageImmediately: prompt,
@@ -385,69 +383,6 @@ extension WindowBaseCommandHandler {
385383

386384
presenter.presentPromptToCode(fileURL: fileURL)
387385
}
388-
389-
private func startChatWithSelection(
390-
editor: EditorContent,
391-
specifiedSystemPrompt: String?,
392-
extraSystemPrompt: String?,
393-
sendingMessageImmediately: String?,
394-
name: String?
395-
) async throws {
396-
presenter.markAsProcessing(true)
397-
defer { presenter.markAsProcessing(false) }
398-
399-
let fileURL = try await Environment.fetchCurrentFileURL()
400-
401-
let code = {
402-
guard let selection = editor.selections.last,
403-
selection.start != selection.end else { return "" }
404-
return editor.selectedCode(in: selection)
405-
}()
406-
407-
let chat = WidgetDataSource.shared.createChatIfNeeded(for: fileURL)
408-
409-
chat.mutateSystemPrompt(specifiedSystemPrompt)
410-
chat.mutateExtraSystemPrompt(extraSystemPrompt ?? "")
411-
412-
Task {
413-
let customCommandPrefix = {
414-
if let name { return "[\(name)] " }
415-
return ""
416-
}()
417-
418-
if specifiedSystemPrompt != nil {
419-
await chat.chatGPTService.mutateHistory { history in
420-
history.append(.init(
421-
role: .assistant,
422-
content: "",
423-
summary: "\(customCommandPrefix)System prompt is updated."
424-
))
425-
}
426-
} else if !code.isEmpty, let selection = editor.selections.last {
427-
await chat.chatGPTService.mutateHistory { history in
428-
history.append(.init(
429-
role: .assistant,
430-
content: "",
431-
summary: "\(customCommandPrefix)Chatting about selected code in `\(fileURL.lastPathComponent)` from `\(selection.start.line + 1):\(selection.start.character + 1)` to `\(selection.end.line + 1):\(selection.end.character)`.\nThe code will persist in the conversation."
432-
))
433-
}
434-
} else if !customCommandPrefix.isEmpty {
435-
await chat.chatGPTService.mutateHistory { history in
436-
history.append(.init(
437-
role: .assistant,
438-
content: "",
439-
summary: "\(customCommandPrefix)System prompt is updated."
440-
))
441-
}
442-
}
443-
444-
if let sendingMessageImmediately, !sendingMessageImmediately.isEmpty {
445-
try await chat.send(content: sendingMessageImmediately)
446-
}
447-
}
448-
449-
presenter.presentChatRoom(fileURL: fileURL)
450-
}
451386

452387
private func startChat(
453388
specifiedSystemPrompt: String?,
@@ -459,7 +394,6 @@ extension WindowBaseCommandHandler {
459394
defer { presenter.markAsProcessing(false) }
460395

461396
let focusedElementURI = try await Environment.fetchFocusedElementURI()
462-
let language = UserDefaults.shared.value(for: \.chatGPTLanguage)
463397

464398
let chat = WidgetDataSource.shared.createChatIfNeeded(for: focusedElementURI)
465399

@@ -472,15 +406,7 @@ extension WindowBaseCommandHandler {
472406
return ""
473407
}()
474408

475-
if specifiedSystemPrompt != nil {
476-
await chat.chatGPTService.mutateHistory { history in
477-
history.append(.init(
478-
role: .assistant,
479-
content: "",
480-
summary: "\(customCommandPrefix)System prompt is updated."
481-
))
482-
}
483-
} else if !customCommandPrefix.isEmpty {
409+
if specifiedSystemPrompt != nil || extraSystemPrompt != nil {
484410
await chat.chatGPTService.mutateHistory { history in
485411
history.append(.init(
486412
role: .assistant,

Core/Sources/SuggestionWidget/ChatProvider.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import Foundation
2+
import Preferences
23
import SwiftUI
34

45
public final class ChatProvider: ObservableObject {
56
let id = UUID()
67
@Published public var history: [ChatMessage] = []
78
@Published public var isReceivingMessage = false
9+
public var systemPrompt = ""
10+
public var extraSystemPrompt = ""
811
public var onMessageSend: (String) -> Void
912
public var onStop: () -> Void
1013
public var onClear: () -> Void
1114
public var onClose: () -> Void
1215
public var onSwitchContext: () -> Void
1316
public var onDeleteMessage: (String) -> Void
1417
public var onResendMessage: (String) -> Void
18+
public var onResetPrompt: () -> Void
19+
public var onRunCustomCommand: (CustomCommand) -> Void = { _ in }
1520

1621
public init(
1722
history: [ChatMessage] = [],
@@ -22,7 +27,9 @@ public final class ChatProvider: ObservableObject {
2227
onClose: @escaping () -> Void = {},
2328
onSwitchContext: @escaping () -> Void = {},
2429
onDeleteMessage: @escaping (String) -> Void = { _ in },
25-
onResendMessage: @escaping (String) -> Void = { _ in }
30+
onResendMessage: @escaping (String) -> Void = { _ in },
31+
onResetPrompt: @escaping () -> Void = {},
32+
onRunCustomCommand: @escaping (CustomCommand) -> Void = { _ in }
2633
) {
2734
self.history = history
2835
self.isReceivingMessage = isReceivingMessage
@@ -33,6 +40,8 @@ public final class ChatProvider: ObservableObject {
3340
self.onSwitchContext = onSwitchContext
3441
self.onDeleteMessage = onDeleteMessage
3542
self.onResendMessage = onResendMessage
43+
self.onResetPrompt = onResetPrompt
44+
self.onRunCustomCommand = onRunCustomCommand
3645
}
3746

3847
public func send(_ message: String) { onMessageSend(message) }
@@ -42,6 +51,10 @@ public final class ChatProvider: ObservableObject {
4251
public func switchContext() { onSwitchContext() }
4352
public func deleteMessage(id: String) { onDeleteMessage(id) }
4453
public func resendMessage(id: String) { onResendMessage(id) }
54+
public func resetPrompt() { onResetPrompt() }
55+
public func triggerCustomCommand(_ command: CustomCommand) {
56+
onRunCustomCommand(command)
57+
}
4558
}
4659

4760
public struct ChatMessage: Equatable {
@@ -55,3 +68,4 @@ public struct ChatMessage: Equatable {
5568
self.text = text
5669
}
5770
}
71+

Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ struct ChatPanelInputArea: View {
258258
}
259259
.padding(8)
260260
.background(.ultraThickMaterial)
261+
.contextMenu {
262+
ChatContextMenu(chat: chat)
263+
}
261264
}
262265

263266
var clearButton: some View {
@@ -342,6 +345,71 @@ struct ChatPanelInputArea: View {
342345
}
343346
}
344347

348+
struct ChatContextMenu: View {
349+
let chat: ChatProvider
350+
@AppStorage(\.customCommands) var customCommands
351+
352+
var body: some View {
353+
Group {
354+
currentSystemPrompt
355+
currentExtraSystemPrompt
356+
resetPrompt
357+
358+
Divider()
359+
360+
customCommandMenu
361+
}
362+
}
363+
364+
@ViewBuilder
365+
var currentSystemPrompt: some View {
366+
Text("System Prompt:")
367+
Text({
368+
var text = chat.systemPrompt
369+
if text.isEmpty { text = "N/A" }
370+
if text.count > 30 { text = String(text.prefix(30)) + "..." }
371+
return text
372+
}() as String)
373+
}
374+
375+
@ViewBuilder
376+
var currentExtraSystemPrompt: some View {
377+
Text("Extra Prompt:")
378+
Text({
379+
var text = chat.extraSystemPrompt
380+
if text.isEmpty { text = "N/A" }
381+
if text.count > 30 { text = String(text.prefix(30)) + "..." }
382+
return text
383+
}() as String)
384+
}
385+
386+
var resetPrompt: some View {
387+
Button("Reset System Prompt") {
388+
chat.resetPrompt()
389+
}
390+
}
391+
392+
var customCommandMenu: some View {
393+
Menu("Custom Commands") {
394+
ForEach(
395+
customCommands.filter {
396+
switch $0.feature {
397+
case .chatWithSelection, .customChat: return true
398+
case .promptToCode: return false
399+
}
400+
},
401+
id: \.name
402+
) { command in
403+
Button(action: {
404+
chat.triggerCustomCommand(command)
405+
}) {
406+
Text(command.name)
407+
}
408+
}
409+
}
410+
}
411+
}
412+
345413
struct RoundedCorners: Shape {
346414
var tl: CGFloat = 0.0
347415
var tr: CGFloat = 0.0
@@ -403,7 +471,7 @@ struct GlobalChatSwitchToggleStyle: ToggleStyle {
403471
HStack(spacing: 4) {
404472
Text(configuration.isOn ? "Shared Conversation" : "Local Conversation")
405473
.foregroundStyle(.tertiary)
406-
474+
407475
RoundedRectangle(cornerRadius: 10, style: .circular)
408476
.foregroundColor(configuration.isOn ? Color.indigo : .gray.opacity(0.5))
409477
.frame(width: 30, height: 20, alignment: .center)

0 commit comments

Comments
 (0)