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

@@ -39,7 +39,7 @@ struct HeaderView: View {
private let gitSync = GitSyncService.shared
var body: some View {
HStack(spacing: 12) {
HStack(spacing: 20) {
// Provider picker dropdown only shows configured providers
Menu {
ForEach(registry.configuredProviders, id: \.self) { p in
@@ -126,6 +126,17 @@ struct HeaderView: View {
.buttonStyle(.plain)
.help("Select model")
if let model = model {
let isFav = settings.favoriteModelIds.contains(model.id)
Button(action: { settings.toggleFavoriteModel(model.id) }) {
Image(systemName: isFav ? "star.fill" : "star")
.font(.system(size: settings.guiTextSize - 3))
.foregroundColor(isFav ? .yellow : .oaiSecondary)
}
.buttonStyle(.plain)
.help(isFav ? "Remove from favorites" : "Add to favorites")
}
Spacer()
// Status indicators

View File

@@ -29,6 +29,7 @@ struct ModelInfoView: View {
let model: ModelInfo
@Environment(\.dismiss) var dismiss
@Bindable private var settings = SettingsService.shared
var body: some View {
VStack(spacing: 0) {
@@ -37,6 +38,15 @@ struct ModelInfoView: View {
Text("Model Info")
.font(.system(size: 18, weight: .bold))
Spacer()
let isFav = settings.favoriteModelIds.contains(model.id)
Button(action: { settings.toggleFavoriteModel(model.id) }) {
Image(systemName: isFav ? "star.fill" : "star")
.font(.system(size: 18))
.foregroundColor(isFav ? .yellow : .secondary)
}
.buttonStyle(.plain)
.help(isFav ? "Remove from favorites" : "Add to favorites")
.padding(.trailing, 8)
Button { dismiss() } label: {
Image(systemName: "xmark.circle.fill")
.font(.title2)

View File

@@ -37,9 +37,11 @@ struct ModelSelectorView: View {
@State private var filterOnline = false
@State private var filterImageGen = false
@State private var filterThinking = false
@State private var filterFavorites = false
@State private var keyboardIndex: Int = -1
@State private var sortOrder: ModelSortOrder = .default
@State private var selectedInfoModel: ModelInfo? = nil
@Bindable private var settings = SettingsService.shared
private var filteredModels: [ModelInfo] {
let q = searchText.lowercased()
@@ -54,13 +56,20 @@ struct ModelSelectorView: View {
let matchesOnline = !filterOnline || model.capabilities.online
let matchesImageGen = !filterImageGen || model.capabilities.imageGeneration
let matchesThinking = !filterThinking || model.capabilities.thinking
let matchesFavorites = !filterFavorites || settings.favoriteModelIds.contains(model.id)
return matchesSearch && matchesVision && matchesTools && matchesOnline && matchesImageGen && matchesThinking
return matchesSearch && matchesVision && matchesTools && matchesOnline && matchesImageGen && matchesThinking && matchesFavorites
}
let favIds = settings.favoriteModelIds
switch sortOrder {
case .default:
return filtered
return filtered.sorted { a, b in
let aFav = favIds.contains(a.id)
let bFav = favIds.contains(b.id)
if aFav != bFav { return aFav }
return false
}
case .priceLowHigh:
return filtered.sorted { $0.pricing.prompt < $1.pricing.prompt }
case .priceHighLow:
@@ -91,6 +100,19 @@ struct ModelSelectorView: View {
Spacer()
// Favorites filter star
Button(action: { filterFavorites.toggle(); keyboardIndex = -1 }) {
Image(systemName: filterFavorites ? "star.fill" : "star")
.font(.caption)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(filterFavorites ? Color.yellow.opacity(0.25) : Color.gray.opacity(0.1))
.foregroundColor(filterFavorites ? .yellow : .secondary)
.cornerRadius(6)
}
.buttonStyle(.plain)
.help("Show favorites only")
// Sort menu
Menu {
ForEach(ModelSortOrder.allCases, id: \.rawValue) { order in
@@ -140,7 +162,9 @@ struct ModelSelectorView: View {
model: model,
isSelected: model.id == selectedModel?.id,
isKeyboardHighlighted: index == keyboardIndex,
isFavorite: settings.favoriteModelIds.contains(model.id),
onSelect: { onSelect(model) },
onFavorite: { settings.toggleFavoriteModel(model.id) },
onInfo: { selectedInfoModel = model }
)
.id(model.id)
@@ -249,14 +273,25 @@ struct ModelRowView: View {
let model: ModelInfo
let isSelected: Bool
var isKeyboardHighlighted: Bool = false
var isFavorite: Bool = false
let onSelect: () -> Void
var onFavorite: (() -> Void)? = nil
let onInfo: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 8) {
// Selectable main content
VStack(alignment: .leading, spacing: 6) {
HStack {
HStack(spacing: 6) {
if let onFavorite {
Button(action: onFavorite) {
Image(systemName: isFavorite ? "star.fill" : "star")
.font(.system(size: 13))
.foregroundColor(isFavorite ? .yellow : .secondary)
}
.buttonStyle(.plain)
.help(isFavorite ? "Remove from favorites" : "Add to favorites")
}
Text(model.name)
.font(.headline)
.foregroundColor(isSelected ? .blue : .primary)

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))
}