New release v2.3.8

This commit is contained in:
2026-03-05 13:17:53 +01:00
parent 305abfa85d
commit 3f9b30bfa1
10 changed files with 393 additions and 28 deletions

View File

@@ -58,6 +58,16 @@ struct SettingsView: View {
@State private var syncTestResult: String?
@State private var isSyncing = false
// Anytype state
@State private var anytypeAPIKey = ""
@State private var anytypeURL = ""
@State private var showAnytypeKey = false
@State private var isTestingAnytype = false
@State private var anytypeTestResult: String?
// Default model picker state
@State private var showDefaultModelPicker = false
// Paperless-NGX state
@State private var paperlessURL = ""
@State private var paperlessToken = ""
@@ -136,14 +146,15 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
tabButton(2, icon: "paintbrush", label: "Appearance")
tabButton(3, icon: "slider.horizontal.3", label: "Advanced")
Divider().frame(height: 44).padding(.horizontal, 8)
Divider().frame(height: 44).padding(.horizontal, 4)
tabButton(6, icon: "command", label: "Shortcuts")
tabButton(7, icon: "brain", label: "Skills")
tabButton(4, icon: "arrow.triangle.2.circlepath", label: "Sync")
tabButton(5, icon: "envelope", label: "Email")
tabButton(8, icon: "doc.text", label: "Paperless", beta: true)
tabButton(9, icon: "icloud.and.arrow.up", label: "Backup")
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")
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
@@ -173,6 +184,8 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
paperlessTab
case 9:
backupTab
case 10:
anytypeTab
default:
generalTab
}
@@ -183,6 +196,20 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
}
.frame(minWidth: 860, idealWidth: 940, minHeight: 620, idealHeight: 760)
.sheet(isPresented: $showDefaultModelPicker) {
ModelSelectorView(
models: chatViewModel?.availableModels ?? [],
selectedModel: chatViewModel?.availableModels.first(where: { $0.id == settingsService.defaultModel }),
onSelect: { model in
let provider = chatViewModel.flatMap { vm in
vm.inferProviderPublic(from: model.id)
} ?? settingsService.defaultProvider
settingsService.defaultModel = model.id
settingsService.defaultProvider = provider
showDefaultModelPicker = false
}
)
}
.sheet(isPresented: $showEmailLog) {
EmailLogView()
}
@@ -364,13 +391,28 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Model Settings")
formSection {
row("Default Model ID") {
TextField("e.g. anthropic/claude-sonnet-4", text: Binding(
get: { settingsService.defaultModel ?? "" },
set: { settingsService.defaultModel = $0.isEmpty ? nil : $0 }
))
.textFieldStyle(.roundedBorder)
.frame(width: 300)
row("Default Model") {
HStack(spacing: 8) {
let modelName: String = {
if let id = settingsService.defaultModel {
return chatViewModel?.availableModels.first(where: { $0.id == id })?.name ?? id
}
return "Not set"
}()
Text(modelName)
.foregroundStyle(settingsService.defaultModel == nil ? .secondary : .primary)
.frame(maxWidth: 240, alignment: .leading)
Button("Choose…") { showDefaultModelPicker = true }
.buttonStyle(.borderless)
if settingsService.defaultModel != nil {
Button("Clear") {
settingsService.defaultModel = nil
settingsService.defaultProvider = .openrouter
}
.buttonStyle(.borderless)
.foregroundStyle(.secondary)
}
}
}
}
}
@@ -1909,6 +1951,117 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
}
}
// MARK: - Anytype Tab
@ViewBuilder
private var anytypeTab: some View {
VStack(alignment: .leading, spacing: 20) {
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Anytype")
formSection {
row("Enable Anytype") {
Toggle("", isOn: $settingsService.anytypeMcpEnabled)
.toggleStyle(.switch)
}
}
}
if settingsService.anytypeMcpEnabled {
VStack(alignment: .leading, spacing: 6) {
sectionHeader("Connection")
formSection {
row("API URL") {
TextField("http://127.0.0.1:31009", text: $anytypeURL)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.onSubmit { settingsService.anytypeMcpURL = anytypeURL }
.onChange(of: anytypeURL) { _, new in settingsService.anytypeMcpURL = new }
}
rowDivider()
row("API Key") {
HStack(spacing: 6) {
if showAnytypeKey {
TextField("", text: $anytypeAPIKey)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 240)
.onSubmit { settingsService.anytypeMcpAPIKey = anytypeAPIKey.isEmpty ? nil : anytypeAPIKey }
.onChange(of: anytypeAPIKey) { _, new in
settingsService.anytypeMcpAPIKey = new.isEmpty ? nil : new
}
} else {
SecureField("", text: $anytypeAPIKey)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 240)
.onSubmit { settingsService.anytypeMcpAPIKey = anytypeAPIKey.isEmpty ? nil : anytypeAPIKey }
.onChange(of: anytypeAPIKey) { _, new in
settingsService.anytypeMcpAPIKey = new.isEmpty ? nil : new
}
}
Button(showAnytypeKey ? "Hide" : "Show") {
showAnytypeKey.toggle()
}
.buttonStyle(.borderless)
.font(.system(size: 13))
}
}
rowDivider()
HStack(spacing: 12) {
Button(action: { Task { await testAnytypeConnection() } }) {
HStack {
if isTestingAnytype {
ProgressView().scaleEffect(0.7).frame(width: 14, height: 14)
} else {
Image(systemName: "checkmark.circle")
}
Text("Test Connection")
}
}
.disabled(isTestingAnytype || !settingsService.anytypeMcpConfigured)
if let result = anytypeTestResult {
Text(result)
.font(.system(size: 13))
.foregroundStyle(result.hasPrefix("") ? .green : .red)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
}
}
VStack(alignment: .leading, spacing: 4) {
Text("How to get your API key:")
.font(.system(size: 13, weight: .medium))
Text("1. Open Anytype → Settings → Integrations")
Text("2. Create a new API key")
Text("3. Paste it above")
}
.font(.system(size: 13))
.foregroundStyle(.secondary)
.padding(.horizontal, 4)
}
}
.onAppear {
anytypeURL = settingsService.anytypeMcpURL
anytypeAPIKey = settingsService.anytypeMcpAPIKey ?? ""
}
}
private func testAnytypeConnection() async {
isTestingAnytype = true
anytypeTestResult = nil
let result = await AnytypeMCPService.shared.testConnection()
await MainActor.run {
switch result {
case .success(let msg):
anytypeTestResult = "\(msg)"
case .failure(let err):
anytypeTestResult = "\(err.localizedDescription)"
}
isTestingAnytype = false
}
}
// MARK: - Backup Tab
@ViewBuilder
@@ -2112,9 +2265,9 @@ 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: 68)
.frame(minWidth: 60)
.padding(.vertical, 6)
.padding(.horizontal, 6)
.padding(.horizontal, 4)
.background(selectedTab == tag ? Color.blue.opacity(0.1) : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 8))
}