Bug gixes, features added, GUI updates and more
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user