Bug gixes, features added, GUI updates and more

This commit is contained in:
2026-02-12 14:29:35 +01:00
parent 52447b5e17
commit 7265d22438
21 changed files with 2187 additions and 123 deletions

View File

@@ -27,13 +27,30 @@ struct SettingsView: View {
@State private var showOAuthCodeField = false
private var oauthService = AnthropicOAuthService.shared
private let labelWidth: CGFloat = 140
private let labelWidth: CGFloat = 160
// Default system prompt
private let defaultSystemPrompt = """
You are a helpful AI assistant. Follow these guidelines:
1. **Accuracy First**: Never invent information or make assumptions. If you're unsure about something, say so clearly.
2. **Ask for Clarification**: When a request is ambiguous or lacks necessary details, ask clarifying questions before proceeding.
3. **Be Honest About Limitations**: If you cannot or should not help with a request, explain why clearly and politely. Don't attempt tasks outside your capabilities.
4. **Stay Grounded**: Base responses on facts and known information. Clearly distinguish between facts, informed opinions, and speculation.
5. **Be Direct**: Provide concise, relevant answers. Avoid unnecessary preambles or apologies.
Remember: It's better to ask questions or admit uncertainty than to provide incorrect or fabricated information.
"""
var body: some View {
VStack(spacing: 0) {
// Title
Text("Settings")
.font(.system(size: 18, weight: .bold))
.font(.system(size: 22, weight: .bold))
.padding(.top, 20)
.padding(.bottom, 12)
@@ -42,6 +59,7 @@ struct SettingsView: View {
Text("General").tag(0)
Text("MCP").tag(1)
Text("Appearance").tag(2)
Text("Advanced").tag(3)
}
.pickerStyle(.segmented)
.padding(.horizontal, 24)
@@ -58,6 +76,8 @@ struct SettingsView: View {
mcpTab
case 2:
appearanceTab
case 3:
advancedTab
default:
generalTab
}
@@ -80,7 +100,7 @@ struct SettingsView: View {
.padding(.horizontal, 24)
.padding(.vertical, 12)
}
.frame(minWidth: 550, idealWidth: 600, minHeight: 500, idealHeight: 650)
.frame(minWidth: 700, idealWidth: 800, minHeight: 600, idealHeight: 750)
}
// MARK: - General Tab
@@ -106,6 +126,7 @@ struct SettingsView: View {
row("OpenRouter") {
SecureField("sk-or-...", text: $openrouterKey)
.textFieldStyle(.roundedBorder)
.font(.system(size: 13))
.onAppear { openrouterKey = settingsService.openrouterAPIKey ?? "" }
.onChange(of: openrouterKey) {
settingsService.openrouterAPIKey = openrouterKey.isEmpty ? nil : openrouterKey
@@ -121,13 +142,13 @@ struct SettingsView: View {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
Text("Logged in via Claude Pro/Max")
.font(.subheadline)
.font(.system(size: 14))
Spacer()
Button("Logout") {
oauthService.logout()
ProviderRegistry.shared.clearCache()
}
.font(.subheadline)
.font(.system(size: 14))
.foregroundStyle(.red)
}
} else if showOAuthCodeField {
@@ -144,11 +165,11 @@ struct SettingsView: View {
oauthCode = ""
oauthError = nil
}
.font(.subheadline)
.font(.system(size: 14))
}
if let error = oauthError {
Text(error)
.font(.caption)
.font(.system(size: 13))
.foregroundStyle(.red)
}
} else {
@@ -161,13 +182,13 @@ struct SettingsView: View {
Image(systemName: "person.circle")
Text("Login with Claude Pro/Max")
}
.font(.subheadline)
.font(.system(size: 14))
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
Text("or")
.font(.caption)
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
@@ -251,9 +272,6 @@ struct SettingsView: View {
))
.textFieldStyle(.roundedBorder)
}
row("") {
Toggle("Streaming Responses", isOn: $settingsService.streamEnabled)
}
divider()
@@ -274,7 +292,7 @@ struct SettingsView: View {
HStack {
Spacer().frame(width: labelWidth + 12)
Text("Controls which messages are written to ~/Library/Logs/oAI.log")
.font(.caption)
.font(.system(size: 13))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -284,10 +302,41 @@ struct SettingsView: View {
@ViewBuilder
private var mcpTab: some View {
// Enable toggle
sectionHeader("MCP")
// Description header
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Image(systemName: "folder.badge.gearshape")
.font(.title2)
.foregroundStyle(.blue)
Text("Model Context Protocol")
.font(.system(size: 18, weight: .semibold))
}
Text("MCP gives the AI controlled access to read and optionally write files on your computer. The AI can search, read, and analyze files in allowed folders to help with coding, analysis, and other tasks.")
.font(.system(size: 14))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 24)
.padding(.top, 12)
.padding(.bottom, 16)
divider()
// Enable toggle with status
sectionHeader("Status")
row("") {
Toggle("MCP Enabled (File Access)", isOn: $settingsService.mcpEnabled)
Toggle("Enable MCP", isOn: $settingsService.mcpEnabled)
}
HStack {
Spacer().frame(width: labelWidth + 12)
HStack(spacing: 4) {
Image(systemName: settingsService.mcpEnabled ? "checkmark.circle.fill" : "circle")
.foregroundStyle(settingsService.mcpEnabled ? .green : .secondary)
.font(.system(size: 13))
Text(settingsService.mcpEnabled ? "Active - AI can access allowed folders" : "Disabled - No file access")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
}
if settingsService.mcpEnabled {
@@ -297,12 +346,22 @@ struct SettingsView: View {
sectionHeader("Allowed Folders")
if mcpService.allowedFolders.isEmpty {
HStack {
Spacer().frame(width: labelWidth + 12)
Text("No folders added")
VStack(spacing: 8) {
Image(systemName: "folder.badge.plus")
.font(.system(size: 32))
.foregroundStyle(.tertiary)
Text("No folders added yet")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.secondary)
.font(.subheadline)
Text("Click 'Add Folder' below to grant AI access to a folder")
.font(.system(size: 13))
.foregroundStyle(.tertiary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
.padding(.horizontal, labelWidth + 24)
} else {
ForEach(Array(mcpService.allowedFolders.enumerated()), id: \.offset) { index, folder in
HStack(spacing: 0) {
@@ -314,7 +373,7 @@ struct SettingsView: View {
Text((folder as NSString).lastPathComponent)
.font(.body)
Text(abbreviatePath(folder))
.font(.caption)
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
.padding(.leading, 6)
@@ -324,7 +383,7 @@ struct SettingsView: View {
} label: {
Image(systemName: "trash.fill")
.foregroundStyle(.red)
.font(.caption)
.font(.system(size: 13))
}
.buttonStyle(.plain)
}
@@ -340,7 +399,7 @@ struct SettingsView: View {
Image(systemName: "plus")
Text("Add Folder...")
}
.font(.subheadline)
.font(.system(size: 14))
}
.buttonStyle(.borderless)
Spacer()
@@ -362,20 +421,46 @@ struct SettingsView: View {
// Permissions
sectionHeader("Permissions")
row("") {
VStack(alignment: .leading, spacing: 6) {
Toggle("Write & Edit Files", isOn: $settingsService.mcpCanWriteFiles)
Toggle("Delete Files", isOn: $settingsService.mcpCanDeleteFiles)
Toggle("Create Directories", isOn: $settingsService.mcpCanCreateDirectories)
Toggle("Move & Copy Files", isOn: $settingsService.mcpCanMoveFiles)
VStack(alignment: .leading, spacing: 0) {
HStack {
Spacer().frame(width: labelWidth + 12)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 6) {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
.font(.system(size: 12))
Text("Read access (always enabled)")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
Text("The AI can read and search files in allowed folders")
.font(.system(size: 12))
.foregroundStyle(.tertiary)
.padding(.leading, 18)
}
Spacer()
}
.padding(.bottom, 12)
HStack {
Spacer().frame(width: labelWidth + 12)
Text("Write Permissions (optional)")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
Spacer()
}
.padding(.bottom, 8)
HStack {
Spacer().frame(width: labelWidth + 12)
VStack(alignment: .leading, spacing: 6) {
Toggle("Write & Edit Files", isOn: $settingsService.mcpCanWriteFiles)
Toggle("Delete Files", isOn: $settingsService.mcpCanDeleteFiles)
Toggle("Create Directories", isOn: $settingsService.mcpCanCreateDirectories)
Toggle("Move & Copy Files", isOn: $settingsService.mcpCanMoveFiles)
}
Spacer()
}
}
HStack {
Spacer().frame(width: labelWidth + 12)
Text("Read access is always available when MCP is enabled. Write permissions must be explicitly enabled.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
divider()
@@ -394,7 +479,7 @@ struct SettingsView: View {
HStack {
Spacer().frame(width: labelWidth + 12)
Text("When enabled, listing and searching skip gitignored files. Write operations always ignore .gitignore.")
.font(.caption)
.font(.system(size: 13))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -411,7 +496,7 @@ struct SettingsView: View {
Slider(value: $settingsService.guiTextSize, in: 10...20, step: 1)
.frame(maxWidth: 200)
Text("\(Int(settingsService.guiTextSize)) pt")
.font(.caption)
.font(.system(size: 13))
.foregroundStyle(.secondary)
.frame(width: 40)
}
@@ -421,7 +506,7 @@ struct SettingsView: View {
Slider(value: $settingsService.dialogTextSize, in: 10...24, step: 1)
.frame(maxWidth: 200)
Text("\(Int(settingsService.dialogTextSize)) pt")
.font(.caption)
.font(.system(size: 13))
.foregroundStyle(.secondary)
.frame(width: 40)
}
@@ -431,18 +516,188 @@ struct SettingsView: View {
Slider(value: $settingsService.inputTextSize, in: 10...24, step: 1)
.frame(maxWidth: 200)
Text("\(Int(settingsService.inputTextSize)) pt")
.font(.caption)
.font(.system(size: 13))
.foregroundStyle(.secondary)
.frame(width: 40)
}
}
}
// MARK: - Advanced Tab
@ViewBuilder
private var advancedTab: some View {
sectionHeader("Response Generation")
row("") {
Toggle("Enable Streaming Responses", isOn: $settingsService.streamEnabled)
}
HStack {
Spacer().frame(width: labelWidth + 12)
Text("Stream responses as they're generated. Disable for single, complete responses.")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
divider()
sectionHeader("Model Parameters")
// Max Tokens
row("Max Tokens") {
HStack(spacing: 8) {
Slider(value: Binding(
get: { Double(settingsService.maxTokens) },
set: { settingsService.maxTokens = Int($0) }
), in: 0...32000, step: 256)
.frame(maxWidth: 250)
Text(settingsService.maxTokens == 0 ? "Default" : "\(settingsService.maxTokens)")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.frame(width: 70, alignment: .leading)
}
}
HStack {
Spacer().frame(width: labelWidth + 12)
Text("Maximum tokens in the response. Set to 0 to use model default. Higher values allow longer responses but cost more.")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
// Temperature
row("Temperature") {
HStack(spacing: 8) {
Slider(value: $settingsService.temperature, in: 0...2, step: 0.1)
.frame(maxWidth: 250)
Text(settingsService.temperature == 0.0 ? "Default" : String(format: "%.1f", settingsService.temperature))
.font(.system(size: 13))
.foregroundStyle(.secondary)
.frame(width: 70, alignment: .leading)
}
}
HStack {
Spacer().frame(width: labelWidth + 12)
VStack(alignment: .leading, spacing: 4) {
Text("Controls randomness. Set to 0 to use model default.")
.font(.system(size: 13))
.foregroundStyle(.secondary)
Text("• Lower (0.0-0.7): More focused, deterministic")
.font(.system(size: 13))
.foregroundStyle(.secondary)
Text("• Higher (0.8-2.0): More creative, random")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
.fixedSize(horizontal: false, vertical: true)
}
divider()
sectionHeader("System Prompts")
// Default prompt (read-only)
VStack(alignment: .leading, spacing: 8) {
HStack {
Spacer().frame(width: labelWidth + 12)
HStack(spacing: 4) {
Text("Default Prompt")
.font(.system(size: 14))
.fontWeight(.medium)
Text("(always used)")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
}
HStack(alignment: .top, spacing: 0) {
Spacer().frame(width: labelWidth + 12)
ScrollView {
Text(defaultSystemPrompt)
.font(.system(size: 13, design: .monospaced))
.foregroundStyle(.secondary)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
}
.frame(height: 160)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(6)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.secondary.opacity(0.2), lineWidth: 1)
)
}
HStack {
Spacer().frame(width: labelWidth + 12)
Text("This default prompt is always included to ensure accurate, helpful responses.")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(.bottom, 8)
// Custom prompt (editable)
VStack(alignment: .leading, spacing: 8) {
HStack {
Spacer().frame(width: labelWidth + 12)
HStack(spacing: 4) {
Text("Your Custom Prompt")
.font(.system(size: 14))
.fontWeight(.medium)
Text("(optional)")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
}
HStack(alignment: .top, spacing: 0) {
Spacer().frame(width: labelWidth + 12)
TextEditor(text: Binding(
get: { settingsService.systemPrompt ?? "" },
set: { settingsService.systemPrompt = $0.isEmpty ? nil : $0 }
))
.font(.system(size: 13, design: .monospaced))
.frame(height: 120)
.padding(8)
.background(Color(NSColor.textBackgroundColor))
.cornerRadius(6)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
)
}
HStack {
Spacer().frame(width: labelWidth + 12)
Text("Add additional instructions here. This will be appended to the default prompt. Leave empty if you don't need custom instructions.")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
divider()
sectionHeader("Info")
HStack {
Spacer().frame(width: labelWidth + 12)
VStack(alignment: .leading, spacing: 8) {
Text("⚠️ These are advanced settings")
.font(.system(size: 13))
.foregroundStyle(.orange)
.fontWeight(.medium)
Text("Changing these values affects how the AI generates responses. The defaults work well for most use cases. Only adjust if you know what you're doing.")
.font(.system(size: 13))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
// MARK: - Layout Helpers
private func sectionHeader(_ title: String) -> some View {
Text(title)
.font(.system(size: 13, weight: .semibold))
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.secondary)
.textCase(.uppercase)
}
@@ -450,7 +705,7 @@ struct SettingsView: View {
private func row<Content: View>(_ label: String, @ViewBuilder content: () -> Content) -> some View {
HStack(alignment: .center, spacing: 12) {
Text(label)
.font(.body)
.font(.system(size: 14))
.frame(width: labelWidth, alignment: .trailing)
content()
}