import Cocoa import UniformTypeIdentifiers // MARK: - History Search Panel (Ctrl+R) class HistorySearchPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate { private var tableView: NSTableView! private var searchField: NSTextField! private var filteredHistory: [String] = [] private var completion: ((String?) -> Void)? init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 600, height: 400), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) title = "Command History (Ctrl+R)" isReleasedWhenClosed = false setupUI() } private func setupUI() { let container = NSView(frame: contentView!.bounds) container.autoresizingMask = [.width, .height] // Search field let searchLabel = NSTextField(labelWithString: "Search:") searchLabel.frame = NSRect(x: 20, y: container.bounds.height - 40, width: 60, height: 20) searchLabel.autoresizingMask = [.minYMargin] searchField = NSTextField(frame: NSRect(x: 85, y: container.bounds.height - 42, width: container.bounds.width - 105, height: 24)) searchField.placeholderString = "Type to search history (fuzzy search)..." searchField.autoresizingMask = [.width, .minYMargin] searchField.delegate = self // Table view let scrollView = NSScrollView(frame: NSRect(x: 20, y: 50, width: container.bounds.width - 40, height: container.bounds.height - 110)) scrollView.autoresizingMask = [.width, .height] scrollView.hasVerticalScroller = true scrollView.borderType = .bezelBorder tableView = NSTableView(frame: scrollView.bounds) tableView.headerView = nil tableView.rowHeight = 24 let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("command")) column.width = scrollView.bounds.width - 20 tableView.addTableColumn(column) tableView.dataSource = self tableView.delegate = self tableView.target = self tableView.doubleAction = #selector(selectCommand) scrollView.documentView = tableView // Buttons let selectButton = NSButton(frame: NSRect(x: container.bounds.width - 120, y: 10, width: 100, height: 30)) selectButton.title = "Use Command" selectButton.bezelStyle = .rounded selectButton.target = self selectButton.action = #selector(selectCommand) selectButton.autoresizingMask = [.minXMargin, .maxYMargin] let cancelButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: 10, width: 100, height: 30)) cancelButton.title = "Cancel" cancelButton.bezelStyle = .rounded cancelButton.target = self cancelButton.action = #selector(cancel) cancelButton.autoresizingMask = [.minXMargin, .maxYMargin] let infoLabel = NSTextField(labelWithString: "Tip: Double-click or press Enter to use a command") infoLabel.frame = NSRect(x: 20, y: 15, width: 400, height: 20) infoLabel.textColor = .secondaryLabelColor infoLabel.font = NSFont.systemFont(ofSize: 11) infoLabel.autoresizingMask = [.maxYMargin] container.addSubview(searchLabel) container.addSubview(searchField) container.addSubview(scrollView) container.addSubview(selectButton) container.addSubview(cancelButton) container.addSubview(infoLabel) contentView = container filteredHistory = HistoryManager.shared.getRecentCommands(50) tableView.reloadData() } func controlTextDidChange(_ obj: Notification) { let query = searchField.stringValue filteredHistory = HistoryManager.shared.searchHistory(query) tableView.reloadData() } @objc private func selectCommand() { let row = tableView.selectedRow guard row >= 0 && row < filteredHistory.count else { return } completion?(filteredHistory[row]) close() } @objc private func cancel() { completion?(nil) close() } func show(relativeTo window: NSWindow, completion: @escaping (String?) -> Void) { self.completion = completion center() searchField.stringValue = "" filteredHistory = HistoryManager.shared.getRecentCommands(50) tableView.reloadData() makeKeyAndOrderFront(nil) searchField.becomeFirstResponder() } // MARK: - NSTableViewDataSource func numberOfRows(in tableView: NSTableView) -> Int { return filteredHistory.count } // MARK: - NSTableViewDelegate func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let text = NSTextField(labelWithString: filteredHistory[row]) text.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) text.lineBreakMode = .byTruncatingTail return text } } // MARK: - Snippet Panel class SnippetPanel: NSPanel, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate { private var tableView: NSTableView! private var searchField: NSTextField! private var filteredSnippets: [SnippetManager.Snippet] = [] private var completion: ((SnippetManager.Snippet?) -> Void)? init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 700, height: 500), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) title = "Command Snippets" isReleasedWhenClosed = false setupUI() } private func setupUI() { let container = NSView(frame: contentView!.bounds) container.autoresizingMask = [.width, .height] // Search field searchField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 42, width: container.bounds.width - 260, height: 24)) searchField.placeholderString = "Search snippets..." searchField.autoresizingMask = [.width, .minYMargin] searchField.delegate = self // Add button let addButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: container.bounds.height - 45, width: 70, height: 30)) addButton.title = "Add" addButton.bezelStyle = .rounded addButton.target = self addButton.action = #selector(addSnippet) addButton.autoresizingMask = [.minXMargin, .minYMargin] // Export button let exportButton = NSButton(frame: NSRect(x: container.bounds.width - 155, y: container.bounds.height - 45, width: 70, height: 30)) exportButton.title = "Export" exportButton.bezelStyle = .rounded exportButton.target = self exportButton.action = #selector(exportSnippets) exportButton.autoresizingMask = [.minXMargin, .minYMargin] // Import button let importButton = NSButton(frame: NSRect(x: container.bounds.width - 80, y: container.bounds.height - 45, width: 70, height: 30)) importButton.title = "Import" importButton.bezelStyle = .rounded importButton.target = self importButton.action = #selector(importSnippets) importButton.autoresizingMask = [.minXMargin, .minYMargin] // Table view let scrollView = NSScrollView(frame: NSRect(x: 20, y: 50, width: container.bounds.width - 40, height: container.bounds.height - 110)) scrollView.autoresizingMask = [.width, .height] scrollView.hasVerticalScroller = true scrollView.borderType = .bezelBorder tableView = NSTableView(frame: scrollView.bounds) tableView.rowHeight = 24 let nameColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("name")) nameColumn.title = "Name" nameColumn.width = 150 let commandColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("command")) commandColumn.title = "Command" commandColumn.width = 350 let categoryColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("category")) categoryColumn.title = "Category" categoryColumn.width = 100 tableView.addTableColumn(nameColumn) tableView.addTableColumn(commandColumn) tableView.addTableColumn(categoryColumn) tableView.dataSource = self tableView.delegate = self tableView.target = self tableView.doubleAction = #selector(selectSnippet) scrollView.documentView = tableView // Empty state label let emptyLabel = NSTextField(wrappingLabelWithString: "No snippets yet.\n\nTo create a snippet:\n1. Select text in the terminal\n2. Right-click and choose 'Save as Snippet'\n\nOr click 'Add New' above.") emptyLabel.frame = NSRect(x: 0, y: 0, width: scrollView.bounds.width - 40, height: 100) emptyLabel.alignment = .center emptyLabel.textColor = .secondaryLabelColor emptyLabel.font = NSFont.systemFont(ofSize: 13) emptyLabel.tag = 999 // Tag to identify empty state label scrollView.addSubview(emptyLabel) // Buttons let useButton = NSButton(frame: NSRect(x: container.bounds.width - 120, y: 10, width: 100, height: 30)) useButton.title = "Use Snippet" useButton.bezelStyle = .rounded useButton.target = self useButton.action = #selector(selectSnippet) useButton.autoresizingMask = [.minXMargin, .maxYMargin] let deleteButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: 10, width: 100, height: 30)) deleteButton.title = "Delete" deleteButton.bezelStyle = .rounded deleteButton.target = self deleteButton.action = #selector(deleteSnippet) deleteButton.autoresizingMask = [.minXMargin, .maxYMargin] let cancelButton = NSButton(frame: NSRect(x: container.bounds.width - 340, y: 10, width: 100, height: 30)) cancelButton.title = "Cancel" cancelButton.bezelStyle = .rounded cancelButton.target = self cancelButton.action = #selector(cancel) cancelButton.autoresizingMask = [.minXMargin, .maxYMargin] container.addSubview(searchField) container.addSubview(addButton) container.addSubview(exportButton) container.addSubview(importButton) container.addSubview(scrollView) container.addSubview(useButton) container.addSubview(deleteButton) container.addSubview(cancelButton) contentView = container filteredSnippets = SnippetManager.shared.getAllSnippets() tableView.reloadData() updateEmptyState() } private func updateEmptyState() { if let scrollView = tableView.enclosingScrollView, let emptyLabel = scrollView.viewWithTag(999) { emptyLabel.isHidden = !filteredSnippets.isEmpty } } func controlTextDidChange(_ obj: Notification) { let query = searchField.stringValue filteredSnippets = SnippetManager.shared.searchSnippets(query) tableView.reloadData() updateEmptyState() } @objc private func addSnippet() { let alert = NSAlert() alert.messageText = "Add New Snippet" alert.informativeText = "Enter snippet details:" let nameField = NSTextField(frame: NSRect(x: 0, y: 90, width: 300, height: 24)) nameField.placeholderString = "Name" let commandField = NSTextField(frame: NSRect(x: 0, y: 60, width: 300, height: 24)) commandField.placeholderString = "Command" let categoryField = NSTextField(frame: NSRect(x: 0, y: 30, width: 300, height: 24)) categoryField.placeholderString = "Category (optional)" let descField = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) descField.placeholderString = "Description (optional)" let view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 120)) view.addSubview(nameField) view.addSubview(commandField) view.addSubview(categoryField) view.addSubview(descField) alert.accessoryView = view alert.addButton(withTitle: "Add") alert.addButton(withTitle: "Cancel") if alert.runModal() == .alertFirstButtonReturn { let snippet = SnippetManager.Snippet( name: nameField.stringValue, command: commandField.stringValue, description: descField.stringValue, category: categoryField.stringValue.isEmpty ? "General" : categoryField.stringValue ) SnippetManager.shared.addSnippet(snippet) filteredSnippets = SnippetManager.shared.getAllSnippets() tableView.reloadData() updateEmptyState() } } @objc private func selectSnippet() { let row = tableView.selectedRow guard row >= 0 && row < filteredSnippets.count else { return } completion?(filteredSnippets[row]) close() } @objc private func deleteSnippet() { let row = tableView.selectedRow guard row >= 0 && row < filteredSnippets.count else { return } let snippet = filteredSnippets[row] let alert = NSAlert() alert.messageText = "Delete Snippet?" alert.informativeText = "Are you sure you want to delete '\(snippet.name)'?" alert.addButton(withTitle: "Delete") alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning if alert.runModal() == .alertFirstButtonReturn { SnippetManager.shared.removeSnippet(snippet.id) filteredSnippets = SnippetManager.shared.getAllSnippets() tableView.reloadData() updateEmptyState() } } @objc private func exportSnippets() { guard let data = SnippetManager.shared.exportSnippets() else { let alert = NSAlert() alert.messageText = "Export Failed" alert.informativeText = "Could not export snippets." alert.alertStyle = .warning alert.runModal() return } let savePanel = NSSavePanel() savePanel.allowedContentTypes = [.json] savePanel.nameFieldStringValue = "snippets.json" savePanel.title = "Export Snippets" if savePanel.runModal() == .OK, let url = savePanel.url { do { try data.write(to: url) let alert = NSAlert() alert.messageText = "Export Successful" alert.informativeText = "Snippets exported to \(url.lastPathComponent)" alert.alertStyle = .informational alert.runModal() } catch { let alert = NSAlert() alert.messageText = "Export Failed" alert.informativeText = error.localizedDescription alert.alertStyle = .warning alert.runModal() } } } @objc private func importSnippets() { let openPanel = NSOpenPanel() openPanel.allowedContentTypes = [.json] openPanel.allowsMultipleSelection = false openPanel.title = "Import Snippets" if openPanel.runModal() == .OK, let url = openPanel.url { do { let data = try Data(contentsOf: url) // Ask if user wants to replace or merge let alert = NSAlert() alert.messageText = "Import Snippets" alert.informativeText = "Do you want to replace existing snippets or merge with them?" alert.addButton(withTitle: "Merge") alert.addButton(withTitle: "Replace") alert.addButton(withTitle: "Cancel") let response = alert.runModal() if response == .alertThirdButtonReturn { return } let replace = (response == .alertSecondButtonReturn) if SnippetManager.shared.importSnippets(from: data, replace: replace) { filteredSnippets = SnippetManager.shared.getAllSnippets() tableView.reloadData() updateEmptyState() let success = NSAlert() success.messageText = "Import Successful" success.informativeText = "Snippets imported successfully." success.alertStyle = .informational success.runModal() } else { let error = NSAlert() error.messageText = "Import Failed" error.informativeText = "Could not parse snippets file." error.alertStyle = .warning error.runModal() } } catch { let alert = NSAlert() alert.messageText = "Import Failed" alert.informativeText = error.localizedDescription alert.alertStyle = .warning alert.runModal() } } } @objc private func cancel() { completion?(nil) close() } func show(relativeTo window: NSWindow, completion: @escaping (SnippetManager.Snippet?) -> Void) { self.completion = completion center() searchField.stringValue = "" filteredSnippets = SnippetManager.shared.getAllSnippets() tableView.reloadData() updateEmptyState() makeKeyAndOrderFront(nil) } // MARK: - NSTableViewDataSource func numberOfRows(in tableView: NSTableView) -> Int { return filteredSnippets.count } // MARK: - NSTableViewDelegate func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let snippet = filteredSnippets[row] let identifier = tableColumn?.identifier.rawValue ?? "" let text: String switch identifier { case "name": text = snippet.name case "command": text = snippet.command case "category": text = snippet.category default: text = "" } let textField = NSTextField(labelWithString: text) textField.font = NSFont.systemFont(ofSize: 12) textField.lineBreakMode = .byTruncatingTail return textField } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 24 } func tableViewSelectionDidChange(_ notification: Notification) { // Update UI based on selection if needed } } // MARK: - AI Generator Panel class AIGeneratorPanel: NSPanel, NSTextFieldDelegate { private var promptField: NSTextField! private var resultField: NSTextView! private var generateButton: NSButton! private var useButton: NSButton! private var statusLabel: NSTextField! private var completion: ((String?) -> Void)? init() { super.init( contentRect: NSRect(x: 0, y: 0, width: 600, height: 350), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) title = "AI Command Generator" isReleasedWhenClosed = false setupUI() } private func setupUI() { let container = NSView(frame: contentView!.bounds) container.autoresizingMask = [.width, .height] // Prompt field let promptLabel = NSTextField(labelWithString: "Describe what you want to do:") promptLabel.frame = NSRect(x: 20, y: container.bounds.height - 35, width: 250, height: 20) promptLabel.autoresizingMask = [.minYMargin] promptField = NSTextField(frame: NSRect(x: 20, y: container.bounds.height - 65, width: container.bounds.width - 40, height: 24)) promptField.placeholderString = "e.g., find all PDF files larger than 10MB" promptField.autoresizingMask = [.width, .minYMargin] promptField.delegate = self // Generate button generateButton = NSButton(frame: NSRect(x: 20, y: container.bounds.height - 105, width: 120, height: 30)) generateButton.title = "Generate" generateButton.bezelStyle = .rounded generateButton.target = self generateButton.action = #selector(generate) generateButton.autoresizingMask = [.minYMargin] // Status label statusLabel = NSTextField(labelWithString: "") statusLabel.frame = NSRect(x: 150, y: container.bounds.height - 100, width: 300, height: 20) statusLabel.textColor = .secondaryLabelColor statusLabel.font = NSFont.systemFont(ofSize: 11) statusLabel.autoresizingMask = [.minYMargin] // Result field let resultLabel = NSTextField(labelWithString: "Generated Command:") resultLabel.frame = NSRect(x: 20, y: container.bounds.height - 135, width: 200, height: 20) resultLabel.autoresizingMask = [.minYMargin] let scrollView = NSScrollView(frame: NSRect(x: 20, y: 60, width: container.bounds.width - 40, height: container.bounds.height - 205)) scrollView.autoresizingMask = [.width, .height] scrollView.hasVerticalScroller = true scrollView.borderType = .bezelBorder resultField = NSTextView(frame: scrollView.bounds) resultField.isEditable = true resultField.font = NSFont.monospacedSystemFont(ofSize: 13, weight: .regular) resultField.string = "Generated command will appear here..." resultField.textColor = .secondaryLabelColor scrollView.documentView = resultField // Buttons useButton = NSButton(frame: NSRect(x: container.bounds.width - 120, y: 15, width: 100, height: 30)) useButton.title = "Use Command" useButton.bezelStyle = .rounded useButton.target = self useButton.action = #selector(useCommand) useButton.autoresizingMask = [.minXMargin, .maxYMargin] let cancelButton = NSButton(frame: NSRect(x: container.bounds.width - 230, y: 15, width: 100, height: 30)) cancelButton.title = "Cancel" cancelButton.bezelStyle = .rounded cancelButton.target = self cancelButton.action = #selector(cancel) cancelButton.autoresizingMask = [.minXMargin, .maxYMargin] let infoLabel = NSTextField(labelWithString: "Tip: You can edit the generated command before using it") infoLabel.frame = NSRect(x: 20, y: 20, width: 400, height: 20) infoLabel.textColor = .secondaryLabelColor infoLabel.font = NSFont.systemFont(ofSize: 11) infoLabel.autoresizingMask = [.maxYMargin] container.addSubview(promptLabel) container.addSubview(promptField) container.addSubview(generateButton) container.addSubview(statusLabel) container.addSubview(resultLabel) container.addSubview(scrollView) container.addSubview(useButton) container.addSubview(cancelButton) container.addSubview(infoLabel) contentView = container } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if commandSelector == #selector(NSResponder.insertNewline(_:)) { generate() return true } return false } @objc private func generate() { let prompt = promptField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) guard !prompt.isEmpty else { statusLabel.stringValue = "Please enter a description" return } generateButton.isEnabled = false statusLabel.stringValue = "Generating..." resultField.string = "Generating command..." resultField.textColor = .secondaryLabelColor AIManager.shared.generateCommand(from: prompt) { [weak self] result in DispatchQueue.main.async { self?.generateButton.isEnabled = true switch result { case .success(let command): self?.resultField.string = command self?.resultField.textColor = .labelColor self?.statusLabel.stringValue = "✓ Generated successfully" case .failure(let error): self?.resultField.string = "Error: \(error.localizedDescription)" self?.resultField.textColor = .systemRed self?.statusLabel.stringValue = "✗ Generation failed" } } } } @objc private func useCommand() { let command = resultField.string.trimmingCharacters(in: .whitespacesAndNewlines) guard !command.isEmpty && !command.hasPrefix("Error:") && !command.hasPrefix("Generating") && !command.hasPrefix("Generated command") else { return } completion?(command) close() } @objc private func cancel() { completion?(nil) close() } func show(relativeTo window: NSWindow, completion: @escaping (String?) -> Void) { self.completion = completion center() promptField.stringValue = "" resultField.string = "Generated command will appear here..." resultField.textColor = .secondaryLabelColor statusLabel.stringValue = "" makeKeyAndOrderFront(nil) promptField.becomeFirstResponder() } } // MARK: - Settings Window class SettingsWindowController: NSWindowController { convenience init() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 500, height: 450), styleMask: [.titled, .closable], backing: .buffered, defer: false ) window.title = "Settings" window.center() window.isReleasedWhenClosed = false self.init(window: window) setupUI() } private func setupUI() { guard let window = window else { return } let container = NSView(frame: window.contentView!.bounds) container.autoresizingMask = [.width, .height] var yPos = container.bounds.height - 40 // Theme selection let themeLabel = NSTextField(labelWithString: "Theme:") themeLabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20) let themePopup = NSPopUpButton(frame: NSRect(x: 150, y: yPos - 5, width: 200, height: 30)) themePopup.addItems(withTitles: TerminalSettings.Theme.allThemes.map { $0.name }) themePopup.selectItem(at: TerminalSettings.Theme.allThemes.firstIndex(where: { $0.name == TerminalSettings.shared.theme.name }) ?? 0) themePopup.target = self themePopup.action = #selector(themeChanged(_:)) container.addSubview(themeLabel) container.addSubview(themePopup) yPos -= 50 // Font size let fontLabel = NSTextField(labelWithString: "Font Size:") fontLabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20) let fontField = NSTextField(frame: NSRect(x: 150, y: yPos, width: 60, height: 24)) fontField.stringValue = "\(Int(TerminalSettings.shared.fontSize))" fontField.target = self fontField.action = #selector(fontSizeChanged(_:)) container.addSubview(fontLabel) container.addSubview(fontField) yPos -= 50 // Auto-suggestions toggle let suggestLabel = NSTextField(labelWithString: "Auto-suggestions:") suggestLabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20) let suggestCheckbox = NSButton(checkboxWithTitle: "Enable", target: self, action: #selector(suggestionsToggled(_:))) suggestCheckbox.frame = NSRect(x: 150, y: yPos - 2, width: 100, height: 24) suggestCheckbox.state = TerminalSettings.shared.enableAutoSuggestions ? .on : .off container.addSubview(suggestLabel) container.addSubview(suggestCheckbox) yPos -= 50 // AI Provider let aiLabel = NSTextField(labelWithString: "AI Provider:") aiLabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20) let aiPopup = NSPopUpButton(frame: NSRect(x: 150, y: yPos - 5, width: 200, height: 30)) aiPopup.addItems(withTitles: TerminalSettings.AIProvider.allCases.map { $0.rawValue }) aiPopup.selectItem(withTitle: TerminalSettings.shared.aiProvider.rawValue) aiPopup.target = self aiPopup.action = #selector(aiProviderChanged(_:)) container.addSubview(aiLabel) container.addSubview(aiPopup) yPos -= 50 // OpenAI Key let openAILabel = NSTextField(labelWithString: "OpenAI Key:") openAILabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20) let openAIField = NSSecureTextField(frame: NSRect(x: 150, y: yPos, width: 320, height: 24)) openAIField.placeholderString = "sk-..." openAIField.stringValue = TerminalSettings.shared.openAIKey openAIField.target = self openAIField.action = #selector(openAIKeyChanged(_:)) container.addSubview(openAILabel) container.addSubview(openAIField) yPos -= 50 // Ollama URL let ollamaLabel = NSTextField(labelWithString: "Ollama URL:") ollamaLabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20) let ollamaField = NSTextField(frame: NSRect(x: 150, y: yPos, width: 320, height: 24)) ollamaField.placeholderString = "http://localhost:11434" ollamaField.stringValue = TerminalSettings.shared.ollamaURL ollamaField.target = self ollamaField.action = #selector(ollamaURLChanged(_:)) container.addSubview(ollamaLabel) container.addSubview(ollamaField) yPos -= 50 // Ollama Model let modelLabel = NSTextField(labelWithString: "Ollama Model:") modelLabel.frame = NSRect(x: 20, y: yPos, width: 120, height: 20) let modelField = NSTextField(frame: NSRect(x: 150, y: yPos, width: 320, height: 24)) modelField.placeholderString = "llama3.2:latest" modelField.stringValue = TerminalSettings.shared.ollamaModel modelField.target = self modelField.action = #selector(ollamaModelChanged(_:)) container.addSubview(modelLabel) container.addSubview(modelField) yPos -= 50 // Info label let infoLabel = NSTextField(wrappingLabelWithString: "Note: For Ollama, make sure it's running locally. For OpenAI, you need an API key from platform.openai.com") infoLabel.frame = NSRect(x: 20, y: 20, width: 460, height: 40) infoLabel.textColor = .secondaryLabelColor infoLabel.font = NSFont.systemFont(ofSize: 11) container.addSubview(infoLabel) window.contentView = container } @objc private func themeChanged(_ sender: NSPopUpButton) { let themes = TerminalSettings.Theme.allThemes if sender.indexOfSelectedItem < themes.count { TerminalSettings.shared.theme = themes[sender.indexOfSelectedItem] NotificationCenter.default.post(name: NSNotification.Name("ThemeChanged"), object: nil) } } @objc private func fontSizeChanged(_ sender: NSTextField) { if let size = Double(sender.stringValue), size >= 8 && size <= 72 { TerminalSettings.shared.fontSize = CGFloat(size) NotificationCenter.default.post(name: NSNotification.Name("ThemeChanged"), object: nil) } } @objc private func suggestionsToggled(_ sender: NSButton) { TerminalSettings.shared.enableAutoSuggestions = sender.state == .on } @objc private func aiProviderChanged(_ sender: NSPopUpButton) { let providers = TerminalSettings.AIProvider.allCases if sender.indexOfSelectedItem < providers.count { TerminalSettings.shared.aiProvider = providers[sender.indexOfSelectedItem] AIManager.shared.updateService() } } @objc private func openAIKeyChanged(_ sender: NSTextField) { TerminalSettings.shared.openAIKey = sender.stringValue AIManager.shared.updateService() } @objc private func ollamaURLChanged(_ sender: NSTextField) { TerminalSettings.shared.ollamaURL = sender.stringValue AIManager.shared.updateService() } @objc private func ollamaModelChanged(_ sender: NSTextField) { TerminalSettings.shared.ollamaModel = sender.stringValue AIManager.shared.updateService() } }