Fixed problems with folder select for MCP
This commit is contained in:
@@ -279,7 +279,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||||
MARKETING_VERSION = 2.3.5;
|
MARKETING_VERSION = 2.3.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
|
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -323,7 +323,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||||
MARKETING_VERSION = 2.3.5;
|
MARKETING_VERSION = 2.3.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
|
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
|
|||||||
@@ -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.
|
/// Re-save the current conversation under its existing name, or prompt if never saved.
|
||||||
func quickSave() {
|
func quickSave() {
|
||||||
let chatMessages = messages.filter { $0.role != .system }
|
let chatMessages = messages.filter { $0.role != .system }
|
||||||
|
|||||||
@@ -96,9 +96,14 @@ struct ContentView: View {
|
|||||||
CreditsView(provider: chatViewModel.currentProvider)
|
CreditsView(provider: chatViewModel.currentProvider)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $vm.showConversations) {
|
.sheet(isPresented: $vm.showConversations) {
|
||||||
ConversationListView(onLoad: { conversation in
|
ConversationListView(
|
||||||
chatViewModel.loadConversation(conversation)
|
onLoad: { conversation in
|
||||||
})
|
chatViewModel.loadConversation(conversation)
|
||||||
|
},
|
||||||
|
onRename: { id, newName in
|
||||||
|
chatViewModel.didRenameConversation(id: id, newName: newName)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.sheet(item: $vm.modelInfoTarget) { model in
|
.sheet(item: $vm.modelInfoTarget) { model in
|
||||||
ModelInfoView(model: model)
|
ModelInfoView(model: model)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ struct ConversationListView: View {
|
|||||||
@FocusState private var searchFocused: Bool
|
@FocusState private var searchFocused: Bool
|
||||||
private let settings = SettingsService.shared
|
private let settings = SettingsService.shared
|
||||||
var onLoad: ((Conversation) -> Void)?
|
var onLoad: ((Conversation) -> Void)?
|
||||||
|
var onRename: ((UUID, String) -> Void)?
|
||||||
|
|
||||||
private var filteredConversations: [Conversation] {
|
private var filteredConversations: [Conversation] {
|
||||||
if searchText.isEmpty {
|
if searchText.isEmpty {
|
||||||
@@ -216,6 +217,16 @@ struct ConversationListView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if !isSelecting {
|
if !isSelecting {
|
||||||
|
Button {
|
||||||
|
renameConversation(conversation)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "pencil")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.font(.system(size: 15))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Rename conversation")
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
deleteConversation(conversation)
|
deleteConversation(conversation)
|
||||||
} label: {
|
} label: {
|
||||||
@@ -240,6 +251,13 @@ struct ConversationListView: View {
|
|||||||
Label("Delete", systemImage: "trash")
|
Label("Delete", systemImage: "trash")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
renameConversation(conversation)
|
||||||
|
} label: {
|
||||||
|
Label("Rename", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
.tint(.orange)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
exportConversation(conversation)
|
exportConversation(conversation)
|
||||||
} label: {
|
} label: {
|
||||||
@@ -315,6 +333,35 @@ struct ConversationListView: View {
|
|||||||
selectedIndex = 0
|
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) {
|
private func deleteConversation(_ conversation: Conversation) {
|
||||||
do {
|
do {
|
||||||
let _ = try DatabaseService.shared.deleteConversation(id: conversation.id)
|
let _ = try DatabaseService.shared.deleteConversation(id: conversation.id)
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ struct SettingsView: View {
|
|||||||
@State private var openaiKey = ""
|
@State private var openaiKey = ""
|
||||||
@State private var googleKey = ""
|
@State private var googleKey = ""
|
||||||
@State private var googleEngineID = ""
|
@State private var googleEngineID = ""
|
||||||
@State private var showFolderPicker = false
|
|
||||||
@State private var selectedTab = 0
|
@State private var selectedTab = 0
|
||||||
|
@State private var isFolderDropTargeted = false
|
||||||
@State private var logLevel: LogLevel = FileLogger.shared.minimumLevel
|
@State private var logLevel: LogLevel = FileLogger.shared.minimumLevel
|
||||||
|
|
||||||
// Git Sync state
|
// 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
|
// Folders
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
sectionHeader("Allowed Folders")
|
sectionHeader("Allowed Folders")
|
||||||
formSection {
|
// Folder list — also a drop target for Finder drags
|
||||||
|
VStack(spacing: 0) {
|
||||||
if mcpService.allowedFolders.isEmpty {
|
if mcpService.allowedFolders.isEmpty {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Image(systemName: "folder.badge.plus")
|
Image(systemName: "folder.badge.plus")
|
||||||
.font(.system(size: 32))
|
.font(.system(size: 32))
|
||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(isFolderDropTargeted ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.tertiary))
|
||||||
Text("No folders added yet")
|
Text(isFolderDropTargeted ? "Drop to add folder" : "No folders added yet")
|
||||||
.font(.system(size: 14, weight: .medium))
|
.font(.system(size: 14, weight: .medium))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(isFolderDropTargeted ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.secondary))
|
||||||
Text("Click 'Add Folder' below to grant AI access to a folder")
|
if !isFolderDropTargeted {
|
||||||
.font(.system(size: 13))
|
Text("Click 'Add Folder' below or drag folders here from Finder")
|
||||||
.foregroundStyle(.tertiary)
|
.font(.system(size: 13))
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 24)
|
.padding(.vertical, 24)
|
||||||
@@ -458,32 +461,51 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.background(.regularMaterial)
|
||||||
HStack {
|
.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 {
|
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: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
Label("Add Folder…", systemImage: "plus")
|
||||||
Image(systemName: "plus")
|
.font(.system(size: 13, weight: .medium))
|
||||||
Text("Add Folder...")
|
.foregroundStyle(.white)
|
||||||
}
|
.padding(.horizontal, 14)
|
||||||
.font(.system(size: 14))
|
.padding(.vertical, 7)
|
||||||
}
|
.background(Color.accentColor)
|
||||||
.buttonStyle(.borderless)
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ struct oAIApp: App {
|
|||||||
Button("Save Chat") { chatViewModel.saveFromMenu() }
|
Button("Save Chat") { chatViewModel.saveFromMenu() }
|
||||||
.keyboardShortcut("s", modifiers: .command)
|
.keyboardShortcut("s", modifiers: .command)
|
||||||
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
.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 }
|
Button("Stats") { chatViewModel.showStats = true }
|
||||||
.keyboardShortcut("s", modifiers: [.command, .shift])
|
.keyboardShortcut("s", modifiers: [.command, .shift])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user