Skip to content

Commit 7d9b5c6

Browse files
committed
✨ Navigate threads via keyboard, store last used for models
1 parent de3269e commit 7d9b5c6

14 files changed

+114
-61
lines changed

DraftPatch/Components/ChatBox.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import SwiftUI
1010
struct ChatBoxView: View {
1111
@Binding var userMessage: String
1212
@Binding var selectedDraftApp: DraftApp?
13-
@FocusState.Binding var isTextFieldFocused: Bool
13+
@Binding var isTextFieldFocused: Bool
1414

1515
let thinking: Bool
1616
let onSubmit: () -> Void

DraftPatch/Components/ChatBoxEditor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import SwiftUI
1010

1111
struct ChatBoxEditor: View {
1212
@Binding var userMessage: String
13-
@FocusState.Binding var isTextFieldFocused: Bool
13+
@Binding var isTextFieldFocused: Bool
1414
@State private var textEditorHeight: CGFloat = 15
1515

1616
let thinking: Bool

DraftPatch/Components/CustomTextEditor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import SwiftUI
1010

1111
struct CustomTextEditor: NSViewRepresentable {
1212
@Binding var text: String
13-
@FocusState.Binding var isFocused: Bool
13+
@Binding var isFocused: Bool
1414

1515
var isEditable: Bool = true
1616
var thinking: Bool = false

DraftPatch/Components/ModelPickerPopoverView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ struct ModelPickerPopoverView: View {
2424

2525
var filteredModels: [ChatModel] {
2626
let sortedModels = viewModel.availableModels.sorted {
27-
($0.lastUsed ?? .distantPast) > ($1.lastUsed ?? .distantPast)
27+
guard let date1 = $0.lastUsed, let date2 = $1.lastUsed else {
28+
return $0.lastUsed != nil
29+
}
30+
31+
return date1 > date2
2832
}
2933

3034
if searchText.isEmpty {

DraftPatch/Components/RenamableTitleView.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ struct RenamableTitleView: View {
1111
@ObservedObject var thread: ChatThread
1212
@State private var isRenaming = false
1313
@State private var localTitle: String
14-
@FocusState private var isTextFieldFocused: Bool
14+
@FocusState private var focused: Bool
1515

1616
init(thread: ChatThread) {
1717
self.thread = thread
@@ -31,19 +31,19 @@ struct RenamableTitleView: View {
3131
.accessibilityIdentifier("renameThreadTextField")
3232
.textFieldStyle(.roundedBorder)
3333
.frame(maxWidth: 200)
34-
.focused($isTextFieldFocused)
34+
.focused($focused)
3535
.onKeyPress { key in
3636
if key.characters == "\u{1B}" {
3737
localTitle = thread.title
38-
isTextFieldFocused = false
38+
focused = false
3939
isRenaming = false
4040
return .handled
4141
}
4242
return .ignored
4343
}
4444
.task {
4545
DispatchQueue.main.async {
46-
isTextFieldFocused = true
46+
focused = true
4747
}
4848
}
4949
} else {

DraftPatch/DraftPatchViewModel.swift

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class DraftPatchViewModel: ObservableObject {
3232
@Published var selectedModel: ChatModel? = nil
3333
@Published var thinking: Bool = false
3434
@Published var showSettings: Bool = false
35+
@Published var chatBoxFocused: Bool = true
3536

3637
@Published var isDraftingEnabled: Bool = false
3738
@Published var selectedDraftApp: DraftApp? = nil {
@@ -48,6 +49,7 @@ class DraftPatchViewModel: ObservableObject {
4849
self.repository = repository
4950
self.llmManager = llmManager
5051

52+
loadModels()
5153
loadSettings()
5254
loadThreads()
5355

@@ -80,6 +82,14 @@ class DraftPatchViewModel: ObservableObject {
8082
}
8183
}
8284

85+
func loadModels() {
86+
do {
87+
availableModels = try repository.fetchModels() ?? []
88+
} catch {
89+
print("Error loading settings: \(error)")
90+
}
91+
}
92+
8393
func deleteThread(_ thread: ChatThread) {
8494
do {
8595
try repository.deleteThread(thread)
@@ -106,27 +116,7 @@ class DraftPatchViewModel: ObservableObject {
106116
}
107117

108118
func loadLLMs() async {
109-
do {
110-
let storedModels = try repository.fetchStoredModels()
111-
self.availableModels = storedModels.map { ChatModel(name: $0.name, provider: $0.provider) }
112-
} catch {
113-
print("Error loading stored models: \(error)")
114-
}
115-
116-
let fetchedModels = await llmManager.loadLLMs(settings)
117-
118-
do {
119-
for model in fetchedModels {
120-
if !self.availableModels.contains(where: { $0.name == model.name && $0.provider == model.provider }) {
121-
let storedModel = ChatModel(name: model.name, provider: model.provider)
122-
try repository.insertStoredModel(storedModel)
123-
}
124-
}
125-
} catch {
126-
print("Error saving fetched models: \(error)")
127-
}
128-
129-
self.availableModels = fetchedModels
119+
self.availableModels = await llmManager.loadLLMs(settings, existingModels: availableModels)
130120
}
131121

132122
func toggleDraftWithLastApp() {
@@ -165,9 +155,12 @@ class DraftPatchViewModel: ObservableObject {
165155
/// we insert that draft into the context before persisting the message.
166156
func sendMessage(_ text: String? = nil) async {
167157
guard let thread = selectedThread else { return }
168-
guard let model = selectedModel ?? availableModels.first else { return }
169-
model.lastUsed = Date()
170-
thread.model = model
158+
guard let currentModel = selectedModel ?? availableModels.first else { return }
159+
160+
if let model = availableModels.first(where: { $0.id == currentModel.id }) {
161+
model.lastUsed = Date()
162+
thread.model = model
163+
}
171164

172165
// Fetch selected text if a DraftApp is selected
173166
let selectedText = selectedDraftApp.flatMap { draftApp in
@@ -309,4 +302,28 @@ class DraftPatchViewModel: ObservableObject {
309302
return try await llmManager.getService(for: model.provider)
310303
.generateTitle(for: text, modelName: model.name)
311304
}
305+
306+
func selectPreviousThread() {
307+
guard !chatThreads.isEmpty else { return }
308+
309+
if let currentThread = selectedThread,
310+
let index = chatThreads.firstIndex(where: { $0.id == currentThread.id })
311+
{
312+
selectedThread = index > 0 ? chatThreads[index - 1] : chatThreads.last
313+
} else {
314+
selectedThread = chatThreads.last
315+
}
316+
}
317+
318+
func selectNextThread() {
319+
guard !chatThreads.isEmpty else { return }
320+
321+
if let currentThread = selectedThread,
322+
let index = chatThreads.firstIndex(where: { $0.id == currentThread.id })
323+
{
324+
selectedThread = index < chatThreads.count - 1 ? chatThreads[index + 1] : chatThreads.first
325+
} else {
326+
selectedThread = chatThreads.first
327+
}
328+
}
312329
}

DraftPatch/Models/ChatModel.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,22 @@ enum LLMProvider: String, Codable {
1616
}
1717

1818
@Model
19-
class ChatModel: Identifiable, Equatable {
19+
final class ChatModel: Identifiable, Equatable {
2020
var name: String
2121
var provider: LLMProvider
22-
var lastUsed: Date? = nil
22+
var lastUsed: Date?
2323

2424
var id: String { name }
2525

2626
init(name: String, provider: LLMProvider, lastUsed: Date? = nil) {
2727
self.name = name
2828
self.provider = provider
29-
self.lastUsed = Date()
29+
self.lastUsed = lastUsed
3030
}
3131

3232
static func == (lhs: ChatModel, rhs: ChatModel) -> Bool {
33-
return lhs.name == rhs.name && lhs.provider == rhs.provider
33+
return lhs.name == rhs.name
34+
&& lhs.provider == rhs.provider
35+
&& lhs.lastUsed == rhs.lastUsed
3436
}
3537
}

DraftPatch/Protocols/DraftPatchRepository.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ protocol DraftPatchRepository {
99
func save() throws
1010
func fetchThreads() throws -> [ChatThread]
1111
func fetchSettings() throws -> Settings?
12+
func fetchModels() throws -> [ChatModel]?
13+
func insertModel(_ model: ChatModel) throws
1214
func insertThread(_ thread: ChatThread) throws
1315
func deleteThread(_ thread: ChatThread) throws
14-
func fetchStoredModels() throws -> [ChatModel]
15-
func insertStoredModel(_ model: ChatModel) throws
16+
1617
}

DraftPatch/RootView.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import SwiftUI
1010

1111
struct RootView: View {
1212
@EnvironmentObject var viewModel: DraftPatchViewModel
13-
@FocusState var isTextFieldFocused: Bool
1413

1514
var body: some View {
1615
NavigationSplitView {
@@ -64,7 +63,7 @@ struct RootView: View {
6463
.background(viewModel.showSettings ? Color.accentColor.opacity(0.2) : Color.clear)
6564
} detail: {
6665
NavigationStack {
67-
ChatView(isTextFieldFocused: $isTextFieldFocused)
66+
ChatView()
6867
.environmentObject(viewModel)
6968
.navigationDestination(isPresented: $viewModel.showSettings) {
7069
SettingsView()
@@ -98,7 +97,7 @@ struct RootView: View {
9897
ToolbarItem(placement: .primaryAction) {
9998
Button {
10099
viewModel.createDraftThread(title: "New Conversation")
101-
isTextFieldFocused = true
100+
viewModel.chatBoxFocused = true
102101
} label: {
103102
Label("New Chat", systemImage: "highlighter")
104103
}

DraftPatch/Services/LLMManager.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class LLMManager {
2121
}
2222
}
2323

24-
func loadLLMs(_ settings: Settings?) async -> [ChatModel] {
24+
func loadLLMs(_ settings: Settings?, existingModels: [ChatModel]) async -> [ChatModel] {
2525
var availableModels: [ChatModel] = []
2626

2727
let providers: [(enabled: Bool, service: LLMService, provider: LLMProvider)] = [
@@ -35,8 +35,17 @@ class LLMManager {
3535
for provider in providers where provider.enabled {
3636
group.addTask {
3737
do {
38-
let models = try await provider.service.fetchAvailableModels()
39-
return models.map { ChatModel(name: $0, provider: provider.provider) }
38+
let fetchedModelNames = try await provider.service.fetchAvailableModels()
39+
40+
return fetchedModelNames.compactMap { modelName in
41+
if let existingModel = existingModels.first(where: {
42+
$0.name == modelName && $0.provider == provider.provider
43+
}) {
44+
return existingModel
45+
} else {
46+
return ChatModel(name: modelName, provider: provider.provider)
47+
}
48+
}
4049
} catch {
4150
print("Error loading \(provider.provider) models: \(error)")
4251
return []

0 commit comments

Comments
 (0)