New release v2.3.8
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user