Fixed problems with folder select for MCP
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user