Fixed problems with folder select for MCP

This commit is contained in:
2026-03-01 18:00:30 +01:00
parent 98d9ee2b51
commit 65a35cd508
6 changed files with 152 additions and 36 deletions

View File

@@ -551,6 +551,46 @@ Don't narrate future actions ("Let me...") - just use the tools.
}
}
/// Always prompts for a new name and saves a fresh copy, switching the session to that copy.
func saveAsFromMenu() {
let chatMessages = messages.filter { $0.role != .system }
guard !chatMessages.isEmpty else { return }
#if os(macOS)
let alert = NSAlert()
alert.messageText = "Save Chat As"
alert.informativeText = "Enter a name for this conversation:"
alert.addButton(withTitle: "Save")
alert.addButton(withTitle: "Cancel")
let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
input.placeholderString = "Conversation name…"
if let existing = currentConversationName {
input.stringValue = existing // pre-fill with current name as a starting point
}
alert.accessoryView = input
alert.window.initialFirstResponder = input
guard alert.runModal() == .alertFirstButtonReturn else { return }
let name = input.stringValue.trimmingCharacters(in: .whitespaces)
guard !name.isEmpty else { return }
do {
let saved = try DatabaseService.shared.saveConversation(name: name, messages: chatMessages)
currentConversationId = saved.id
currentConversationName = name
savedMessageCount = chatMessages.count
showSystemMessage("Saved as \"\(name)\"")
} catch {
showSystemMessage("Save failed: \(error.localizedDescription)")
}
#endif
}
/// Called by ConversationListView after renaming a saved conversation.
/// Updates the in-session name if the renamed conversation is currently open.
func didRenameConversation(id: UUID, newName: String) {
if currentConversationId == id {
currentConversationName = newName
}
}
/// Re-save the current conversation under its existing name, or prompt if never saved.
func quickSave() {
let chatMessages = messages.filter { $0.role != .system }

View File

@@ -96,9 +96,14 @@ struct ContentView: View {
CreditsView(provider: chatViewModel.currentProvider)
}
.sheet(isPresented: $vm.showConversations) {
ConversationListView(onLoad: { conversation in
chatViewModel.loadConversation(conversation)
})
ConversationListView(
onLoad: { conversation in
chatViewModel.loadConversation(conversation)
},
onRename: { id, newName in
chatViewModel.didRenameConversation(id: id, newName: newName)
}
)
}
.sheet(item: $vm.modelInfoTarget) { model in
ModelInfoView(model: model)

View File

@@ -39,6 +39,7 @@ struct ConversationListView: View {
@FocusState private var searchFocused: Bool
private let settings = SettingsService.shared
var onLoad: ((Conversation) -> Void)?
var onRename: ((UUID, String) -> Void)?
private var filteredConversations: [Conversation] {
if searchText.isEmpty {
@@ -216,6 +217,16 @@ struct ConversationListView: View {
Spacer()
if !isSelecting {
Button {
renameConversation(conversation)
} label: {
Image(systemName: "pencil")
.foregroundStyle(.secondary)
.font(.system(size: 15))
}
.buttonStyle(.plain)
.help("Rename conversation")
Button {
deleteConversation(conversation)
} label: {
@@ -240,6 +251,13 @@ struct ConversationListView: View {
Label("Delete", systemImage: "trash")
}
Button {
renameConversation(conversation)
} label: {
Label("Rename", systemImage: "pencil")
}
.tint(.orange)
Button {
exportConversation(conversation)
} label: {
@@ -315,6 +333,35 @@ struct ConversationListView: View {
selectedIndex = 0
}
private func renameConversation(_ conversation: Conversation) {
#if os(macOS)
let alert = NSAlert()
alert.messageText = "Rename Conversation"
alert.addButton(withTitle: "Rename")
alert.addButton(withTitle: "Cancel")
let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
input.stringValue = conversation.name
input.selectText(nil)
alert.accessoryView = input
alert.window.initialFirstResponder = input
guard alert.runModal() == .alertFirstButtonReturn else { return }
let newName = input.stringValue.trimmingCharacters(in: .whitespaces)
guard !newName.isEmpty, newName != conversation.name else { return }
do {
_ = try DatabaseService.shared.updateConversation(id: conversation.id, name: newName, messages: nil)
withAnimation {
if let i = conversations.firstIndex(where: { $0.id == conversation.id }) {
conversations[i].name = newName
conversations[i].updatedAt = Date()
}
}
onRename?(conversation.id, newName)
} catch {
Log.db.error("Failed to rename conversation: \(error.localizedDescription)")
}
#endif
}
private func deleteConversation(_ conversation: Conversation) {
do {
let _ = try DatabaseService.shared.deleteConversation(id: conversation.id)

View File

@@ -42,8 +42,8 @@ struct SettingsView: View {
@State private var openaiKey = ""
@State private var googleKey = ""
@State private var googleEngineID = ""
@State private var showFolderPicker = false
@State private var selectedTab = 0
@State private var isFolderDropTargeted = false
@State private var logLevel: LogLevel = FileLogger.shared.minimumLevel
// Git Sync state
@@ -412,18 +412,21 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
// Folders
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Allowed Folders")
formSection {
// Folder list also a drop target for Finder drags
VStack(spacing: 0) {
if mcpService.allowedFolders.isEmpty {
VStack(spacing: 8) {
Image(systemName: "folder.badge.plus")
.font(.system(size: 32))
.foregroundStyle(.tertiary)
Text("No folders added yet")
.foregroundStyle(isFolderDropTargeted ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.tertiary))
Text(isFolderDropTargeted ? "Drop to add folder" : "No folders added yet")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.secondary)
Text("Click 'Add Folder' below to grant AI access to a folder")
.font(.system(size: 13))
.foregroundStyle(.tertiary)
.foregroundStyle(isFolderDropTargeted ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.secondary))
if !isFolderDropTargeted {
Text("Click 'Add Folder' below or drag folders here from Finder")
.font(.system(size: 13))
.foregroundStyle(.tertiary)
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
@@ -458,32 +461,51 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
}
}
}
}
HStack {
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(
isFolderDropTargeted ? Color.accentColor : Color.primary.opacity(0.10),
lineWidth: isFolderDropTargeted ? 2 : 0.5
)
)
.onDrop(of: [.fileURL], isTargeted: $isFolderDropTargeted) { providers in
for provider in providers {
_ = provider.loadObject(ofClass: URL.self) { url, _ in
guard let url, url.hasDirectoryPath else { return }
DispatchQueue.main.async {
withAnimation { _ = mcpService.addFolder(url.path) }
}
}
}
return true
}
// Add Folder button
Button {
showFolderPicker = true
let panel = NSOpenPanel()
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.allowsMultipleSelection = true
panel.prompt = "Add"
panel.message = "Choose folders to allow AI access"
panel.begin { response in
guard response == .OK else { return }
for url in panel.urls {
withAnimation { _ = mcpService.addFolder(url.path) }
}
}
} label: {
HStack(spacing: 4) {
Image(systemName: "plus")
Text("Add Folder...")
}
.font(.system(size: 14))
}
.buttonStyle(.borderless)
Spacer()
}
.padding(.horizontal, 4)
.fileImporter(
isPresented: $showFolderPicker,
allowedContentTypes: [.folder],
allowsMultipleSelection: false
) { result in
if case .success(let urls) = result, let url = urls.first {
if url.startAccessingSecurityScopedResource() {
withAnimation { _ = mcpService.addFolder(url.path) }
url.stopAccessingSecurityScopedResource()
}
Label("Add Folder…", systemImage: "plus")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, 14)
.padding(.vertical, 7)
.background(Color.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
}
// Permissions

View File

@@ -98,6 +98,8 @@ struct oAIApp: App {
Button("Save Chat") { chatViewModel.saveFromMenu() }
.keyboardShortcut("s", modifiers: .command)
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
Button("Save Chat As…") { chatViewModel.saveAsFromMenu() }
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
Button("Stats") { chatViewModel.showStats = true }
.keyboardShortcut("s", modifiers: [.command, .shift])
}