New release v2.3.9

- Jarvis integration: manage oAI-Web agents and usage from inside the app (/jarvis command, Settings tab 11)
- Model category filter: keyword-based categorisation with popover picker in model selector
- Categories shown in ModelInfoView with coloured chips; dot indicators on model rows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 11:05:47 +02:00
parent c2010e272e
commit 13699864d8
15 changed files with 1795 additions and 8 deletions
+114 -1
View File
@@ -65,6 +65,13 @@ struct SettingsView: View {
@State private var isTestingAnytype = false
@State private var anytypeTestResult: String?
// Jarvis state
@State private var jarvisURL = ""
@State private var jarvisAPIKey = ""
@State private var showJarvisKey = false
@State private var isTestingJarvis = false
@State private var jarvisTestResult: String?
// Default model picker state
@State private var showDefaultModelPicker = false
@@ -155,6 +162,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
tabButton(8, icon: "doc.text", label: "Paperless", beta: true)
tabButton(9, icon: "icloud.and.arrow.up", label: "Backup")
tabButton(10, icon: "square.stack.3d.up", label: "Anytype")
tabButton(11, icon: "server.rack", label: "Jarvis")
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
@@ -186,6 +194,8 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
backupTab
case 10:
anytypeTab
case 11:
jarvisTab
default:
generalTab
}
@@ -2047,6 +2057,107 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
}
}
// MARK: - Jarvis Tab
private var jarvisTab: some View {
VStack(alignment: .leading, spacing: 20) {
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Jarvis")
formSection {
row("Enable Jarvis") {
Toggle("", isOn: $settingsService.jarvisEnabled)
.toggleStyle(.switch)
}
}
}
if settingsService.jarvisEnabled {
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Connection")
formSection {
row("Server URL") {
TextField("https://jarvis.example.com", text: $jarvisURL)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.onSubmit { settingsService.jarvisURL = jarvisURL }
.onChange(of: jarvisURL) { _, new in settingsService.jarvisURL = new }
}
rowDivider()
row("API Key") {
HStack(spacing: 6) {
if showJarvisKey {
TextField("", text: $jarvisAPIKey)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 240)
.onSubmit { settingsService.jarvisAPIKey = jarvisAPIKey.isEmpty ? nil : jarvisAPIKey }
.onChange(of: jarvisAPIKey) { _, new in
settingsService.jarvisAPIKey = new.isEmpty ? nil : new
}
} else {
SecureField("", text: $jarvisAPIKey)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 240)
.onSubmit { settingsService.jarvisAPIKey = jarvisAPIKey.isEmpty ? nil : jarvisAPIKey }
.onChange(of: jarvisAPIKey) { _, new in
settingsService.jarvisAPIKey = new.isEmpty ? nil : new
}
}
Button(showJarvisKey ? "Hide" : "Show") {
showJarvisKey.toggle()
}
.buttonStyle(.borderless)
.font(.system(size: 13))
}
}
rowDivider()
HStack(spacing: 12) {
Button(action: { Task { await testJarvisConnection() } }) {
HStack {
if isTestingJarvis {
ProgressView().scaleEffect(0.7).frame(width: 14, height: 14)
} else {
Image(systemName: "checkmark.circle")
}
Text("Test Connection")
}
}
.disabled(isTestingJarvis || !settingsService.jarvisConfigured)
if let result = jarvisTestResult {
Text(result)
.font(.system(size: 13))
.foregroundStyle(result.hasPrefix("") ? .green : .red)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Generate an API key in your Jarvis settings and paste it above.")
}
.font(.system(size: 13))
.foregroundStyle(.secondary)
.padding(.horizontal, 4)
}
}
.onAppear {
jarvisURL = settingsService.jarvisURL
jarvisAPIKey = settingsService.jarvisAPIKey ?? ""
}
}
private func testJarvisConnection() async {
isTestingJarvis = true
jarvisTestResult = nil
let ok = await JarvisService.shared.testConnection()
await MainActor.run {
jarvisTestResult = ok ? "✓ Connected" : "✗ Connection failed"
isTestingJarvis = false
}
}
private func testAnytypeConnection() async {
isTestingAnytype = true
anytypeTestResult = nil
@@ -2265,7 +2376,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
.font(.system(size: 11))
.foregroundStyle(selectedTab == tag ? .blue : .secondary)
}
.frame(minWidth: 60)
.frame(minWidth: 55)
.padding(.vertical, 6)
.padding(.horizontal, 4)
.background(selectedTab == tag ? Color.blue.opacity(0.1) : Color.clear)
@@ -2286,6 +2397,8 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
case 7: return "Skills"
case 8: return "Paperless"
case 9: return "Backup"
case 10: return "Anytype"
case 11: return "Jarvis"
default: return "Settings"
}
}