Files
nucleic/Sources/NucleicApp/SettingsView.swift
T
abkslmandClaude Opus 4.8 4c8b2c6a6c Add a default autoship toggle to settings
New chats can default to autoship via a Settings toggle in New chat
defaults. Enabling it implies Auto mode, mirroring the per-session
control. Persisted under nucleic.defaultAutoShip and loaded into the
store on launch.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 22:26:42 -07:00

117 lines
6.1 KiB
Swift

import SwiftUI
import NucleicCore
/// App settings: defaults applied to new chats, plus the Apple Intelligence model
/// used for summaries and auto-generated names.
struct SettingsView: View {
@Environment(AppStore.self) private var store
@AppStorage(IntelligenceChoice.storageKey) private var choiceRaw = IntelligenceChoice.onDevice.rawValue
@AppStorage(ModelCatalog.defaultModelKey) private var defaultModel = ModelCatalog.fallbackModel
@AppStorage(ModelCatalog.defaultEffortKey) private var defaultEffort = ModelCatalog.fallbackEffort
@AppStorage(ModelCatalog.defaultAutoKey) private var defaultAuto = false
@AppStorage(ModelCatalog.defaultAutoShipKey) private var defaultAutoShip = false
@AppStorage(AppTextSize.storageKey) private var textSizeRaw = AppTextSize.medium.rawValue
@AppStorage(ColorVisionMode.storageKey) private var colorVisionRaw = ColorVisionMode.standard.rawValue
@AppStorage(AppAppearance.storageKey) private var appearanceRaw = AppAppearance.dark.rawValue
@AppStorage(TranscriptDisplay.showDebugKey) private var showDebugLines = false
@AppStorage(StreakBadge.showKey) private var showStreak = true
@AppStorage(TriageLabelStyle.encouragingKey) private var encouragingTriageLabels = true
private var choice: IntelligenceChoice { IntelligenceChoice(rawValue: choiceRaw) ?? .onDevice }
private var textSize: AppTextSize { AppTextSize(rawValue: textSizeRaw) ?? .medium }
private var appearance: AppAppearance { AppAppearance(rawValue: appearanceRaw) ?? .dark }
var body: some View {
Form {
Section("Appearance") {
Picker("Theme", selection: $appearanceRaw) {
ForEach(AppAppearance.allCases) { Text($0.label).tag($0.rawValue) }
}
.pickerStyle(.segmented)
Picker("Text size", selection: $textSizeRaw) {
ForEach(AppTextSize.allCases) { Text($0.label).tag($0.rawValue) }
}
Picker("Color vision", selection: $colorVisionRaw) {
ForEach(ColorVisionMode.allCases) { Text($0.label).tag($0.rawValue) }
}
Text("Color vision remaps status dots, the activity grid, and accents to stay distinguishable.")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Toggle("Show streak counter", isOn: $showStreak)
Text("Shows a day-streak badge next to the home greeting, counting consecutive days with activity.")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Toggle("Encouraging to-do labels", isOn: $encouragingTriageLabels)
Text(encouragingTriageLabels
? "Triage badges read as opportunities: Max Impact, High Impact, Worthwhile, Someday."
: "Triage badges use the classic severity words: Critical, High, Medium, Low.")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Section("Transcript") {
Toggle("Show advanced detail", isOn: $showDebugLines)
Text("Adds debug-level lines like rate limits, token usage, and turn markers to the chat. Thinking and tool calls always show.")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Section("New chat defaults") {
Picker("Default model", selection: $defaultModel) {
ForEach(ModelCatalog.models, id: \.self) { Text(ModelCatalog.displayName($0)).tag($0) }
}
Picker("Default effort", selection: $defaultEffort) {
ForEach(ModelCatalog.efforts, id: \.self) { Text($0).tag($0) }
}
Toggle("Start new chats in Auto mode", isOn: $defaultAuto)
Text("Auto mode lets Claude auto-approve safe actions; destructive ones still ask.")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Toggle("Autoship new chats", isOn: $defaultAutoShip)
Text("Autoship merges a finished chat's branch back into the project automatically. It implies Auto mode.")
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Section("Apple Intelligence") {
Picker("Model", selection: $choiceRaw) {
ForEach(IntelligenceChoice.allCases) { Text($0.label).tag($0.rawValue) }
}
.pickerStyle(.radioGroup)
Text(choice.detail)
.font(.caption).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Section("Status") {
LabeledContent("On-device model", value: IntelligenceAvailability.status(for: .onDevice))
LabeledContent("Private Cloud Compute", value: IntelligenceAvailability.status(for: .privateCloudCompute))
}
.font(.callout)
RemoteAccessSection()
}
.formStyle(.grouped)
.frame(width: 480)
.padding()
.preferredColorScheme(appearance.colorScheme)
.dynamicTypeSize(textSize.dynamicTypeSize)
.onAppear { pushDefaults() }
.onChange(of: defaultModel) { _, _ in pushDefaults() }
.onChange(of: defaultEffort) { _, _ in pushDefaults() }
.onChange(of: defaultAuto) { _, _ in pushDefaults() }
.onChange(of: defaultAutoShip) { _, _ in
if defaultAutoShip { defaultAuto = true } // shipping implies autonomous
pushDefaults()
}
}
private func pushDefaults() {
store.defaultModel = defaultModel
store.defaultEffort = defaultEffort
store.defaultAuto = defaultAuto
store.defaultAutoShip = defaultAutoShip
}
}