Skip to content

Commit b9029ca

Browse files
committed
Pre-release 0.31.105
1 parent 090e1e6 commit b9029ca

File tree

49 files changed

+1285
-362
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1285
-362
lines changed

Core/Package.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ let package = Package(
191191
.product(name: "Cache", package: "Tool"),
192192
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
193193
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
194-
.product(name: "SwiftUIFlowLayout", package: "swiftui-flow-layout")
194+
.product(name: "SwiftUIFlowLayout", package: "swiftui-flow-layout"),
195+
.product(name: "Persist", package: "Tool")
195196
]
196197
),
197198
.testTarget(

Core/Sources/ChatService/ChatService.swift

+19-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Status
1010

1111
public protocol ChatServiceType {
1212
var memory: ContextAwareAutoManagedChatMemory { get set }
13-
func send(_ id: String, content: String, skillSet: [ConversationSkill], references: [FileReference]) async throws
13+
func send(_ id: String, content: String, skillSet: [ConversationSkill], references: [FileReference], model: String?) async throws
1414
func stopReceivingMessage() async
1515
func upvote(_ id: String, _ rating: ConversationRating) async
1616
func downvote(_ id: String, _ rating: ConversationRating) async
@@ -82,7 +82,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
8282
return ChatService(provider: provider)
8383
}
8484

85-
public func send(_ id: String, content: String, skillSet: Array<ConversationSkill>, references: Array<FileReference>) async throws {
85+
public func send(_ id: String, content: String, skillSet: Array<ConversationSkill>, references: Array<FileReference>, model: String? = nil) async throws {
8686
guard activeRequestId == nil else { return }
8787
let workDoneToken = UUID().uuidString
8888
activeRequestId = workDoneToken
@@ -115,7 +115,8 @@ public final class ChatService: ChatServiceType, ObservableObject {
115115
workspaceFolder: "",
116116
skills: skillCapabilities,
117117
ignoredSkills: ignoredSkills,
118-
references: references)
118+
references: references,
119+
model: model)
119120
self.skillSet = skillSet
120121
try await send(request)
121122
}
@@ -258,6 +259,11 @@ public final class ChatService: ChatServiceType, ObservableObject {
258259
return nil
259260
}
260261

262+
public func copilotModels() async -> [CopilotModel] {
263+
guard let models = try? await conversationProvider?.models() else { return [] }
264+
return models
265+
}
266+
261267
public func handleSingleRoundDialogCommand(
262268
systemPrompt: String?,
263269
overwriteSystemPrompt: Bool,
@@ -334,6 +340,16 @@ public final class ChatService: ChatServiceType, ObservableObject {
334340
await memory.removeMessage(progress.turnId)
335341
await memory.appendMessage(errorMessage)
336342
}
343+
} else if CLSError.code == 400 && CLSError.message.contains("model is not supported") {
344+
Task {
345+
let errorMessage = ChatMessage(
346+
id: progress.turnId,
347+
role: .assistant,
348+
content: "",
349+
errorMessage: "Oops, the model is not supported. Please enable it first in [GitHub Copilot settings](https://github.com/settings/copilot)."
350+
)
351+
await memory.appendMessage(errorMessage)
352+
}
337353
} else {
338354
Task {
339355
let errorMessage = ChatMessage(

Core/Sources/ConversationTab/Chat.swift

+5-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ChatAPIService
55
import Preferences
66
import Terminal
77
import ConversationServiceProvider
8+
import Persist
89

910
public struct DisplayedChatMessage: Equatable {
1011
public enum Role: Equatable {
@@ -140,19 +141,20 @@ struct Chat {
140141
state.typedMessage = ""
141142

142143
let selectedFiles = state.selectedFiles
143-
144+
let selectedModelFamily = AppState.shared.getSelectedModelFamily()
144145
return .run { _ in
145-
try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles)
146+
try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles, model: selectedModelFamily)
146147
}.cancellable(id: CancelID.sendMessage(self.id))
147148

148149
case let .followUpButtonClicked(id, message):
149150
guard !message.isEmpty else { return .none }
150151
let skillSet = state.buildSkillSet()
151152

152153
let selectedFiles = state.selectedFiles
154+
let selectedModelFamily = AppState.shared.getSelectedModelFamily()
153155

154156
return .run { _ in
155-
try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles)
157+
try await service.send(id, content: message, skillSet: skillSet, references: selectedFiles, model: selectedModelFamily)
156158
}.cancellable(id: CancelID.sendMessage(self.id))
157159

158160
case .returnButtonTapped:

Core/Sources/ConversationTab/ChatPanel.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public struct ChatPanel: View {
3131
} else {
3232
ChatPanelMessages(chat: chat)
3333
.accessibilityElement(children: .combine)
34-
.accessibilityLabel("Chat Mesessages Group")
34+
.accessibilityLabel("Chat Messages Group")
3535

3636
if chat.history.last?.role == .system {
3737
ChatCLSError(chat: chat).padding(.trailing, 16)
@@ -567,6 +567,7 @@ struct ChatPanelInputArea: View {
567567

568568
Spacer()
569569

570+
ModelPicker()
570571
Button(action: {
571572
submitChatMessage()
572573
}) {
@@ -676,7 +677,7 @@ struct ChatPanelInputArea: View {
676677
id: "releaseNotes",
677678
description: "What's New",
678679
shortDescription: "What's New",
679-
scopes: [ChatPromptTemplateScope.chatPanel]
680+
scopes: [PromptTemplateScope.chatPanel]
680681
)
681682

682683
guard !promptTemplates.isEmpty else {

Core/Sources/ConversationTab/ChatTemplateDropdownView.swift

+67-64
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ConversationServiceProvider
22
import AppKit
33
import SwiftUI
4+
import ComposableArchitecture
45

56
public struct ChatTemplateDropdownView: View {
67
@Binding var templates: [ChatTemplate]
@@ -10,76 +11,78 @@ public struct ChatTemplateDropdownView: View {
1011
@State private var localMonitor: Any? = nil
1112

1213
public var body: some View {
13-
VStack(alignment: .leading, spacing: 0) {
14-
ForEach(Array(templates.enumerated()), id: \.element.id) { index, template in
15-
HStack {
16-
Text("/" + template.id)
17-
.hoverPrimaryForeground(isHovered: selectedIndex == index)
18-
Spacer()
19-
Text(template.shortDescription)
20-
.hoverSecondaryForeground(isHovered: selectedIndex == index)
21-
}
22-
.padding(.horizontal, 8)
23-
.padding(.vertical, 6)
24-
.contentShape(Rectangle())
25-
.onTapGesture {
26-
onSelect(template)
27-
}
28-
.hoverBackground(isHovered: selectedIndex == index)
29-
.onHover { isHovered in
30-
if isHovered {
31-
selectedIndex = index
14+
WithPerceptionTracking {
15+
VStack(alignment: .leading, spacing: 0) {
16+
ForEach(Array(templates.enumerated()), id: \.element.id) { index, template in
17+
HStack {
18+
Text("/" + template.id)
19+
.hoverPrimaryForeground(isHovered: selectedIndex == index)
20+
Spacer()
21+
Text(template.shortDescription)
22+
.hoverSecondaryForeground(isHovered: selectedIndex == index)
23+
}
24+
.padding(.horizontal, 8)
25+
.padding(.vertical, 6)
26+
.contentShape(Rectangle())
27+
.onTapGesture {
28+
onSelect(template)
29+
}
30+
.hoverBackground(isHovered: selectedIndex == index)
31+
.onHover { isHovered in
32+
if isHovered {
33+
selectedIndex = index
34+
}
3235
}
3336
}
3437
}
35-
}
36-
.background(
37-
GeometryReader { geometry in
38-
Color.clear
39-
.onAppear { frameHeight = geometry.size.height }
40-
.onChange(of: geometry.size.height) { newHeight in
41-
frameHeight = newHeight
42-
}
38+
.background(
39+
GeometryReader { geometry in
40+
Color.clear
41+
.onAppear { frameHeight = geometry.size.height }
42+
.onChange(of: geometry.size.height) { newHeight in
43+
frameHeight = newHeight
44+
}
45+
}
46+
)
47+
.background(.ultraThickMaterial)
48+
.cornerRadius(6)
49+
.shadow(radius: 2)
50+
.overlay(
51+
RoundedRectangle(cornerRadius: 6)
52+
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
53+
)
54+
.frame(maxWidth: .infinity)
55+
.offset(y: -1 * frameHeight)
56+
.onChange(of: templates) { _ in
57+
selectedIndex = 0
4358
}
44-
)
45-
.background(.ultraThickMaterial)
46-
.cornerRadius(6)
47-
.shadow(radius: 2)
48-
.overlay(
49-
RoundedRectangle(cornerRadius: 6)
50-
.stroke(Color(nsColor: .separatorColor), lineWidth: 1)
51-
)
52-
.frame(maxWidth: .infinity)
53-
.offset(y: -1 * frameHeight)
54-
.onChange(of: templates) { _ in
55-
selectedIndex = 0
56-
}
57-
.onAppear {
58-
selectedIndex = 0
59-
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
60-
switch event.keyCode {
61-
case 126: // Up arrow
62-
moveSelection(up: true)
63-
return nil
64-
case 125: // Down arrow
65-
moveSelection(up: false)
66-
return nil
67-
case 36: // Return key
68-
handleEnter()
69-
return nil
70-
case 48: // Tab key
71-
handleTab()
72-
return nil // not forwarding the Tab Event which will replace the typed message to "\t"
73-
default:
74-
break
59+
.onAppear {
60+
selectedIndex = 0
61+
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
62+
switch event.keyCode {
63+
case 126: // Up arrow
64+
moveSelection(up: true)
65+
return nil
66+
case 125: // Down arrow
67+
moveSelection(up: false)
68+
return nil
69+
case 36: // Return key
70+
handleEnter()
71+
return nil
72+
case 48: // Tab key
73+
handleTab()
74+
return nil // not forwarding the Tab Event which will replace the typed message to "\t"
75+
default:
76+
break
77+
}
78+
return event
7579
}
76-
return event
7780
}
78-
}
79-
.onDisappear {
80-
if let monitor = localMonitor {
81-
NSEvent.removeMonitor(monitor)
82-
localMonitor = nil
81+
.onDisappear {
82+
if let monitor = localMonitor {
83+
NSEvent.removeMonitor(monitor)
84+
localMonitor = nil
85+
}
8386
}
8487
}
8588
}

Core/Sources/ConversationTab/ContextUtils.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import XcodeInspector
33
import Foundation
44
import Logger
55

6-
public let supportedFileExtensions: Set<String> = ["swift", "m", "mm", "h", "cpp", "c", "js", "py", "rb", "java", "applescript", "scpt", "plist", "entitlements"]
6+
public let supportedFileExtensions: Set<String> = ["swift", "m", "mm", "h", "cpp", "c", "js", "py", "rb", "java", "applescript", "scpt", "plist", "entitlements", "md", "json", "xml", "txt", "yaml", "yml"]
77
private let skipPatterns: [String] = [
88
".git",
99
".svn",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import SwiftUI
2+
import ChatService
3+
import Persist
4+
import ComposableArchitecture
5+
6+
public let SELECTED_LLM_KEY = "selectedLLM"
7+
8+
extension AppState {
9+
func getSelectedModelFamily() -> String? {
10+
if let savedModel = get(key: SELECTED_LLM_KEY),
11+
let modelFamily = savedModel["modelFamily"]?.stringValue {
12+
return modelFamily
13+
}
14+
return nil
15+
}
16+
17+
func getSelectedModelName() -> String? {
18+
if let savedModel = get(key: SELECTED_LLM_KEY),
19+
let modelName = savedModel["modelName"]?.stringValue {
20+
return modelName
21+
}
22+
return nil
23+
}
24+
25+
func setSelectedModel(_ model: LLMModel) {
26+
update(key: SELECTED_LLM_KEY, value: model)
27+
}
28+
}
29+
30+
struct LLMModel: Codable, Hashable {
31+
let modelName: String
32+
let modelFamily: String
33+
}
34+
35+
let defaultModel = LLMModel(modelName: "GPT-4o", modelFamily: "gpt-4o")
36+
struct ModelPicker: View {
37+
@State private var selectedModel = defaultModel.modelName
38+
@State private var models: [LLMModel] = [ defaultModel ]
39+
@State private var isHovered = false
40+
@State private var isPressed = false
41+
42+
init() {
43+
self.updateCurrentModel()
44+
}
45+
46+
func updateCurrentModel() {
47+
selectedModel = AppState.shared.getSelectedModelName() ?? defaultModel.modelName
48+
}
49+
50+
var body: some View {
51+
WithPerceptionTracking {
52+
Menu(selectedModel) {
53+
ForEach(models, id: \.self) { option in
54+
Button {
55+
selectedModel = option.modelName
56+
AppState.shared.setSelectedModel(option)
57+
} label: {
58+
if selectedModel == option.modelName {
59+
Text("\(option.modelName)")
60+
} else {
61+
Text(" \(option.modelName)")
62+
}
63+
}
64+
}
65+
}
66+
.menuStyle(BorderlessButtonMenuStyle())
67+
.frame(maxWidth: labelWidth())
68+
.padding(4)
69+
.background(
70+
RoundedRectangle(cornerRadius: 5)
71+
.fill(isHovered ? Color.gray.opacity(0.1) : Color.clear)
72+
)
73+
.onHover { hovering in
74+
isHovered = hovering
75+
}
76+
.onAppear() {
77+
Task {
78+
updateCurrentModel()
79+
self.models = await ChatService.shared.copilotModels().filter(
80+
{ $0.scopes.contains(.chatPanel) }
81+
).map {
82+
LLMModel(modelName: $0.modelName, modelFamily: $0.modelFamily)
83+
}
84+
}
85+
}
86+
.help("Pick Model")
87+
}
88+
}
89+
90+
func labelWidth() -> CGFloat {
91+
let font = NSFont.systemFont(ofSize: NSFont.systemFontSize)
92+
let attributes = [NSAttributedString.Key.font: font]
93+
let width = selectedModel.size(withAttributes: attributes).width
94+
return CGFloat(width + 20)
95+
}
96+
}
97+
98+
struct ModelPicker_Previews: PreviewProvider {
99+
static var previews: some View {
100+
ModelPicker()
101+
}
102+
}

Core/Sources/ConversationTab/Views/BotMessage.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ struct BotMessage: View {
120120
if errorMessage != nil {
121121
HStack(spacing: 4) {
122122
Image(systemName: "info.circle")
123-
Text(errorMessage!)
124-
.font(.system(size: chatFontSize))
123+
ThemedMarkdownText(text: errorMessage!, chat: chat)
125124
}
126125
}
127126

0 commit comments

Comments
 (0)