macOS: add settings previews

This commit is contained in:
Peter Steinberger
2025-12-09 18:04:11 +01:00
parent d5cd1058ab
commit 959ba94eca
16 changed files with 360 additions and 58 deletions

View File

@@ -177,3 +177,13 @@ private struct AboutMetaRow: View {
} }
} }
} }
#if DEBUG
struct AboutSettings_Previews: PreviewProvider {
private static let updater = DisabledUpdaterController()
static var previews: some View {
AboutSettings(updater: updater)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -5,99 +5,116 @@ import SwiftUI
@MainActor @MainActor
final class AppState: ObservableObject { final class AppState: ObservableObject {
private let isPreview: Bool
private func ifNotPreview(_ action: () -> Void) {
guard !self.isPreview else { return }
action()
}
enum ConnectionMode: String { enum ConnectionMode: String {
case local case local
case remote case remote
} }
@Published var isPaused: Bool { @Published var isPaused: Bool {
didSet { UserDefaults.standard.set(self.isPaused, forKey: pauseDefaultsKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.isPaused, forKey: pauseDefaultsKey) } }
} }
@Published var launchAtLogin: Bool { @Published var launchAtLogin: Bool {
didSet { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } } didSet { self.ifNotPreview { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } } }
} }
@Published var onboardingSeen: Bool { @Published var onboardingSeen: Bool {
didSet { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") } didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") } }
} }
@Published var debugPaneEnabled: Bool { @Published var debugPaneEnabled: Bool {
didSet { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } didSet { self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } }
} }
@Published var swabbleEnabled: Bool { @Published var swabbleEnabled: Bool {
didSet { didSet {
UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey) self.ifNotPreview {
Task { await VoiceWakeRuntime.shared.refresh(state: self) } UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey)
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
} }
} }
@Published var swabbleTriggerWords: [String] { @Published var swabbleTriggerWords: [String] {
didSet { didSet {
// Preserve the raw editing state; sanitization happens when we actually use the triggers. // Preserve the raw editing state; sanitization happens when we actually use the triggers.
UserDefaults.standard.set(self.swabbleTriggerWords, forKey: swabbleTriggersKey) self.ifNotPreview {
if self.swabbleEnabled { UserDefaults.standard.set(self.swabbleTriggerWords, forKey: swabbleTriggersKey)
Task { await VoiceWakeRuntime.shared.refresh(state: self) } if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
} }
} }
} }
@Published var voiceWakeTriggerChime: VoiceWakeChime { @Published var voiceWakeTriggerChime: VoiceWakeChime {
didSet { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) } didSet { self.ifNotPreview { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) } }
} }
@Published var voiceWakeSendChime: VoiceWakeChime { @Published var voiceWakeSendChime: VoiceWakeChime {
didSet { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) } didSet { self.ifNotPreview { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) } }
} }
@Published var iconAnimationsEnabled: Bool { @Published var iconAnimationsEnabled: Bool {
didSet { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } }
} }
@Published var showDockIcon: Bool { @Published var showDockIcon: Bool {
didSet { didSet {
UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey) self.ifNotPreview {
AppActivationPolicy.apply(showDockIcon: self.showDockIcon) UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey)
AppActivationPolicy.apply(showDockIcon: self.showDockIcon)
}
} }
} }
@Published var voiceWakeMicID: String { @Published var voiceWakeMicID: String {
didSet { didSet {
UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey) self.ifNotPreview {
if self.swabbleEnabled { UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey)
Task { await VoiceWakeRuntime.shared.refresh(state: self) } if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
} }
} }
} }
@Published var voiceWakeLocaleID: String { @Published var voiceWakeLocaleID: String {
didSet { didSet {
UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey) self.ifNotPreview {
if self.swabbleEnabled { UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey)
Task { await VoiceWakeRuntime.shared.refresh(state: self) } if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
} }
} }
} }
@Published var voiceWakeAdditionalLocaleIDs: [String] { @Published var voiceWakeAdditionalLocaleIDs: [String] {
didSet { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) } }
} }
@Published var voiceWakeForwardEnabled: Bool { @Published var voiceWakeForwardEnabled: Bool {
didSet { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) } }
} }
@Published var voiceWakeForwardCommand: String { @Published var voiceWakeForwardCommand: String {
didSet { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) } }
} }
@Published var voicePushToTalkEnabled: Bool { @Published var voicePushToTalkEnabled: Bool {
didSet { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } }
} }
@Published var iconOverride: IconOverrideSelection { @Published var iconOverride: IconOverrideSelection {
didSet { UserDefaults.standard.set(self.iconOverride.rawValue, forKey: iconOverrideKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconOverride.rawValue, forKey: iconOverrideKey) } }
} }
@Published var isWorking: Bool = false @Published var isWorking: Bool = false
@@ -106,38 +123,41 @@ final class AppState: ObservableObject {
@Published var sendCelebrationTick: Int = 0 @Published var sendCelebrationTick: Int = 0
@Published var heartbeatsEnabled: Bool { @Published var heartbeatsEnabled: Bool {
didSet { didSet {
UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey) self.ifNotPreview {
Task { _ = await AgentRPC.shared.setHeartbeatsEnabled(self.heartbeatsEnabled) } UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey)
Task { _ = await AgentRPC.shared.setHeartbeatsEnabled(self.heartbeatsEnabled) }
}
} }
} }
@Published var connectionMode: ConnectionMode { @Published var connectionMode: ConnectionMode {
didSet { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) } }
} }
@Published var webChatEnabled: Bool { @Published var webChatEnabled: Bool {
didSet { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) } }
} }
@Published var webChatPort: Int { @Published var webChatPort: Int {
didSet { UserDefaults.standard.set(self.webChatPort, forKey: webChatPortKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatPort, forKey: webChatPortKey) } }
} }
@Published var remoteTarget: String { @Published var remoteTarget: String {
didSet { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) } }
} }
@Published var remoteIdentity: String { @Published var remoteIdentity: String {
didSet { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) } }
} }
@Published var remoteProjectRoot: String { @Published var remoteProjectRoot: String {
didSet { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) } didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) } }
} }
private var earBoostTask: Task<Void, Never>? private var earBoostTask: Task<Void, Never>?
init() { init(preview: Bool = false) {
self.isPreview = preview
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey) self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
self.launchAtLogin = false self.launchAtLogin = false
self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen") self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen")
@@ -199,16 +219,20 @@ final class AppState: ObservableObject {
let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey) let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey)
self.webChatPort = storedPort > 0 ? storedPort : 18788 self.webChatPort = storedPort > 0 ? storedPort : 18788
Task.detached(priority: .utility) { [weak self] in if !self.isPreview {
let current = await LaunchAgentManager.status() Task.detached(priority: .utility) { [weak self] in
await MainActor.run { [weak self] in self?.launchAtLogin = current } let current = await LaunchAgentManager.status()
await MainActor.run { [weak self] in self?.launchAtLogin = current }
}
} }
if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() { if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() {
self.swabbleEnabled = false self.swabbleEnabled = false
} }
Task { await VoiceWakeRuntime.shared.refresh(state: self) } if !self.isPreview {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
} }
func triggerVoiceEars(ttl: TimeInterval? = 5) { func triggerVoiceEars(ttl: TimeInterval? = 5) {
@@ -243,14 +267,15 @@ final class AppState: ObservableObject {
return return
} }
self.swabbleEnabled = enabled
guard !self.isPreview else { return }
if !enabled { if !enabled {
self.swabbleEnabled = false
Task { await VoiceWakeRuntime.shared.refresh(state: self) } Task { await VoiceWakeRuntime.shared.refresh(state: self) }
return return
} }
if PermissionManager.voiceWakePermissionsGranted() { if PermissionManager.voiceWakePermissionsGranted() {
self.swabbleEnabled = true
Task { await VoiceWakeRuntime.shared.refresh(state: self) } Task { await VoiceWakeRuntime.shared.refresh(state: self) }
return return
} }
@@ -280,6 +305,37 @@ final class AppState: ObservableObject {
} }
} }
extension AppState {
static var preview: AppState {
let state = AppState(preview: true)
state.isPaused = false
state.launchAtLogin = true
state.onboardingSeen = true
state.debugPaneEnabled = true
state.swabbleEnabled = true
state.swabbleTriggerWords = ["Claude", "Computer", "Jarvis"]
state.voiceWakeTriggerChime = .system(name: "Glass")
state.voiceWakeSendChime = .system(name: "Ping")
state.iconAnimationsEnabled = true
state.showDockIcon = true
state.voiceWakeMicID = "BuiltInMic"
state.voiceWakeLocaleID = Locale.current.identifier
state.voiceWakeAdditionalLocaleIDs = ["en-US", "de-DE"]
state.voiceWakeForwardEnabled = true
state.voiceWakeForwardCommand = defaultVoiceWakeForwardCommand
state.voicePushToTalkEnabled = false
state.iconOverride = .system
state.heartbeatsEnabled = true
state.connectionMode = .local
state.webChatEnabled = true
state.webChatPort = 18788
state.remoteTarget = "user@example.com"
state.remoteIdentity = "~/.ssh/id_ed25519"
state.remoteProjectRoot = "~/Projects/clawdis"
return state
}
}
@MainActor @MainActor
enum AppStateStore { enum AppStateStore {
static let shared = AppState() static let shared = AppState()

View File

@@ -2,6 +2,7 @@ import SwiftUI
@MainActor @MainActor
struct ConfigSettings: View { struct ConfigSettings: View {
private let isPreview = ProcessInfo.processInfo.isPreview
@State private var configModel: String = "" @State private var configModel: String = ""
@State private var customModel: String = "" @State private var customModel: String = ""
@State private var configSaving = false @State private var configSaving = false
@@ -131,6 +132,7 @@ struct ConfigSettings: View {
} }
.task { .task {
guard !self.hasLoaded else { return } guard !self.hasLoaded else { return }
guard !self.isPreview else { return }
self.hasLoaded = true self.hasLoaded = true
self.loadConfig() self.loadConfig()
await self.loadModels() await self.loadModels()
@@ -247,3 +249,12 @@ struct ConfigSettings: View {
return "Context window: \(human) tokens" return "Context window: \(human) tokens"
} }
} }
#if DEBUG
struct ConfigSettings_Previews: PreviewProvider {
static var previews: some View {
ConfigSettings()
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -36,7 +36,7 @@ enum DebugActions {
_ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil) _ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil)
} }
static func sendDebugVoice() async -> Result<String, String> { static func sendDebugVoice() async -> Result<String, DebugActionError> {
let message = """ let message = """
This is a debug test from the Mac app. Reply with "Debug test works (and a funny pun)" \ This is a debug test from the Mac app. Reply with "Debug test works (and a funny pun)" \
if you received that. if you received that.
@@ -51,7 +51,7 @@ enum DebugActions {
return .success("Forwarded. Await reply.") return .success("Forwarded. Await reply.")
case let .failure(error): case let .failure(error):
let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
return .failure("Forward failed: \(detail)") return .failure(.message("Forward failed: \(detail)"))
} }
} }
@@ -75,11 +75,11 @@ enum DebugActions {
let detail = (reason?.isEmpty == false) let detail = (reason?.isEmpty == false)
? reason! ? reason!
: "No error returned. Check /tmp/clawdis.log or rpc output." : "No error returned. Check /tmp/clawdis.log or rpc output."
return .failure("Local send failed: \(detail)") return .failure(.message("Local send failed: \(detail)"))
} }
} catch { } catch {
let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
return .failure("Local send failed: \(detail)") return .failure(.message("Local send failed: \(detail)"))
} }
} }
@@ -91,7 +91,7 @@ enum DebugActions {
} }
} }
private static func pinoLogPath() -> String { static func pinoLogPath() -> String {
let df = DateFormatter() let df = DateFormatter()
df.calendar = Calendar(identifier: .iso8601) df.calendar = Calendar(identifier: .iso8601)
df.locale = Locale(identifier: "en_US_POSIX") df.locale = Locale(identifier: "en_US_POSIX")
@@ -102,3 +102,13 @@ enum DebugActions {
return "/tmp/clawdis.log" return "/tmp/clawdis.log"
} }
} }
enum DebugActionError: LocalizedError {
case message(String)
var errorDescription: String? {
switch self {
case let .message(text): text
}
}
}

View File

@@ -3,6 +3,7 @@ import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
struct DebugSettings: View { struct DebugSettings: View {
private let isPreview = ProcessInfo.processInfo.isPreview
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath @AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
@AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0 @AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0
@AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue @AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue
@@ -44,9 +45,9 @@ struct DebugSettings: View {
} }
LabeledContent("PID") { Text("\(ProcessInfo.processInfo.processIdentifier)") } LabeledContent("PID") { Text("\(ProcessInfo.processInfo.processIdentifier)") }
LabeledContent("Log file") { LabeledContent("Log file") {
Button("Open pino log") { self.openLog() } Button("Open pino log") { DebugActions.openLog() }
.help(self.pinoLogPath) .help(DebugActions.pinoLogPath())
Text(self.pinoLogPath) Text(DebugActions.pinoLogPath())
.font(.caption2.monospaced()) .font(.caption2.monospaced())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.textSelection(.enabled) .textSelection(.enabled)
@@ -205,6 +206,7 @@ struct DebugSettings: View {
.padding(.vertical, 8) .padding(.vertical, 8)
} }
.task { .task {
guard !self.isPreview else { return }
await self.reloadModels() await self.reloadModels()
self.loadSessionStorePath() self.loadSessionStorePath()
} }
@@ -256,9 +258,9 @@ struct DebugSettings: View {
case let .success(message): case let .success(message):
self.debugSendStatus = message self.debugSendStatus = message
self.debugSendError = nil self.debugSendError = nil
case let .failure(message): case let .failure(error):
self.debugSendStatus = message self.debugSendStatus = nil
self.debugSendError = nil self.debugSendError = error.localizedDescription
} }
} }
} }
@@ -348,3 +350,12 @@ struct DebugSettings: View {
.appendingPathComponent("clawdis.json") .appendingPathComponent("clawdis.json")
} }
} }
#if DEBUG
struct DebugSettings_Previews: PreviewProvider {
static var previews: some View {
DebugSettings()
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -238,7 +238,7 @@ private actor GatewayChannelActor {
if let urlError = error as? URLError { if let urlError = error as? URLError {
let desc = urlError.localizedDescription.isEmpty ? "cancelled" : urlError.localizedDescription let desc = urlError.localizedDescription.isEmpty ? "cancelled" : urlError.localizedDescription
return NSError( return NSError(
domain: urlError.errorDomain, domain: URLError.errorDomain,
code: urlError.errorCode, code: urlError.errorCode,
userInfo: [NSLocalizedDescriptionKey: "\(context): \(desc)"]) userInfo: [NSLocalizedDescriptionKey: "\(context): \(desc)"])
} }

View File

@@ -13,6 +13,7 @@ struct GeneralSettings: View {
@State private var relayInstalling = false @State private var relayInstalling = false
@State private var remoteStatus: RemoteStatus = .idle @State private var remoteStatus: RemoteStatus = .idle
@State private var showRemoteAdvanced = false @State private var showRemoteAdvanced = false
private let isPreview = ProcessInfo.processInfo.isPreview
var body: some View { var body: some View {
ScrollView(.vertical) { ScrollView(.vertical) {
@@ -65,6 +66,7 @@ struct GeneralSettings: View {
.padding(.bottom, 16) .padding(.bottom, 16)
} }
.onAppear { .onAppear {
guard !self.isPreview else { return }
self.refreshCLIStatus() self.refreshCLIStatus()
self.refreshRelayStatus() self.refreshRelayStatus()
} }
@@ -527,3 +529,12 @@ private func healthAgeString(_ ms: Double?) -> String {
guard let ms else { return "unknown" } guard let ms else { return "unknown" }
return msToAge(ms) return msToAge(ms)
} }
#if DEBUG
struct GeneralSettings_Previews: PreviewProvider {
static var previews: some View {
GeneralSettings(state: .preview)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -1,7 +1,11 @@
import SwiftUI import SwiftUI
struct InstancesSettings: View { struct InstancesSettings: View {
@StateObject private var store = InstancesStore.shared @ObservedObject var store: InstancesStore
init(store: InstancesStore = .shared) {
self.store = store
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@@ -87,3 +91,12 @@ struct InstancesSettings: View {
.font(.footnote) .font(.footnote)
} }
} }
#if DEBUG
struct InstancesSettings_Previews: PreviewProvider {
static var previews: some View {
InstancesSettings(store: .preview())
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -28,6 +28,7 @@ struct InstanceInfo: Identifiable, Codable {
@MainActor @MainActor
final class InstancesStore: ObservableObject { final class InstancesStore: ObservableObject {
static let shared = InstancesStore() static let shared = InstancesStore()
let isPreview: Bool
@Published var instances: [InstanceInfo] = [] @Published var instances: [InstanceInfo] = []
@Published var lastError: String? @Published var lastError: String?
@@ -39,7 +40,12 @@ final class InstancesStore: ObservableObject {
private let interval: TimeInterval = 30 private let interval: TimeInterval = 30
private var observers: [NSObjectProtocol] = [] private var observers: [NSObjectProtocol] = []
init(isPreview: Bool = false) {
self.isPreview = isPreview
}
func start() { func start() {
guard !self.isPreview else { return }
guard self.task == nil else { return } guard self.task == nil else { return }
self.observeGatewayEvents() self.observeGatewayEvents()
self.task = Task.detached { [weak self] in self.task = Task.detached { [weak self] in
@@ -295,3 +301,33 @@ final class InstancesStore: ObservableObject {
} }
} }
} }
extension InstancesStore {
static func preview(instances: [InstanceInfo] = [
InstanceInfo(
id: "local",
host: "steipete-mac",
ip: "10.0.0.12",
version: "1.2.3",
lastInputSeconds: 12,
mode: "local",
reason: "preview",
text: "Local node: steipete-mac (10.0.0.12) · app 1.2.3",
ts: Date().timeIntervalSince1970 * 1000),
InstanceInfo(
id: "relay",
host: "relay",
ip: "100.64.0.2",
version: "1.2.3",
lastInputSeconds: 45,
mode: "remote",
reason: "preview",
text: "Relay node · tunnel ok",
ts: Date().timeIntervalSince1970 * 1000 - 45_000),
]) -> InstancesStore {
let store = InstancesStore(isPreview: true)
store.instances = instances
store.statusMessage = "Preview data"
return store
}
}

View File

@@ -118,3 +118,23 @@ struct PermissionRow: View {
} }
} }
} }
#if DEBUG
struct PermissionsSettings_Previews: PreviewProvider {
static var previews: some View {
PermissionsSettings(
status: [
.appleScript: true,
.notifications: true,
.accessibility: false,
.screenRecording: false,
.microphone: true,
.speechRecognition: false,
],
refresh: {},
showOnboarding: {}
)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -95,6 +95,49 @@ struct SessionDefaults {
let contextTokens: Int let contextTokens: Int
} }
extension SessionRow {
static var previewRows: [SessionRow] {
[
SessionRow(
id: "direct-1",
key: "user@example.com",
kind: .direct,
updatedAt: Date().addingTimeInterval(-90),
sessionId: "sess-direct-1234",
thinkingLevel: "low",
verboseLevel: "info",
systemSent: false,
abortedLastRun: false,
tokens: SessionTokenStats(input: 320, output: 680, total: 1000, contextTokens: 200_000),
model: "claude-3.5-sonnet"),
SessionRow(
id: "group-1",
key: "group:engineering",
kind: .group,
updatedAt: Date().addingTimeInterval(-3600),
sessionId: "sess-group-4321",
thinkingLevel: "medium",
verboseLevel: nil,
systemSent: true,
abortedLastRun: true,
tokens: SessionTokenStats(input: 5000, output: 1200, total: 6200, contextTokens: 200_000),
model: "claude-opus-4-5"),
SessionRow(
id: "global",
key: "global",
kind: .global,
updatedAt: Date().addingTimeInterval(-86_400),
sessionId: nil,
thinkingLevel: nil,
verboseLevel: nil,
systemSent: false,
abortedLastRun: false,
tokens: SessionTokenStats(input: 150, output: 220, total: 370, contextTokens: 200_000),
model: "gpt-4.1-mini"),
]
}
}
struct ModelChoice: Identifiable, Hashable { struct ModelChoice: Identifiable, Hashable {
let id: String let id: String
let name: String let name: String

View File

@@ -3,13 +3,23 @@ import SwiftUI
@MainActor @MainActor
struct SessionsSettings: View { struct SessionsSettings: View {
@State private var rows: [SessionRow] = [] private let isPreview: Bool
@State private var rows: [SessionRow]
@State private var storePath: String = SessionLoader.defaultStorePath @State private var storePath: String = SessionLoader.defaultStorePath
@State private var lastLoaded: Date? @State private var lastLoaded: Date?
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var loading = false @State private var loading = false
@State private var hasLoaded = false @State private var hasLoaded = false
init(rows: [SessionRow]? = nil, isPreview: Bool = ProcessInfo.processInfo.isPreview) {
self._rows = State(initialValue: rows ?? [])
self.isPreview = isPreview
if isPreview {
self._lastLoaded = State(initialValue: Date())
self._hasLoaded = State(initialValue: true)
}
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 14) {
self.header self.header
@@ -22,6 +32,7 @@ struct SessionsSettings: View {
.padding(.horizontal, 12) .padding(.horizontal, 12)
.task { .task {
guard !self.hasLoaded else { return } guard !self.hasLoaded else { return }
guard !self.isPreview else { return }
self.hasLoaded = true self.hasLoaded = true
await self.refresh() await self.refresh()
} }
@@ -149,6 +160,7 @@ struct SessionsSettings: View {
private func refresh() async { private func refresh() async {
guard !self.loading else { return } guard !self.loading else { return }
guard !self.isPreview else { return }
self.loading = true self.loading = true
self.errorMessage = nil self.errorMessage = nil
@@ -209,3 +221,12 @@ private struct Badge: View {
.clipShape(Capsule()) .clipShape(Capsule())
} }
} }
#if DEBUG
struct SessionsSettings_Previews: PreviewProvider {
static var previews: some View {
SessionsSettings(rows: SessionRow.previewRows, isPreview: true)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -6,6 +6,13 @@ struct SettingsRootView: View {
@State private var monitoringPermissions = false @State private var monitoringPermissions = false
@State private var selectedTab: SettingsTab = .general @State private var selectedTab: SettingsTab = .general
let updater: UpdaterProviding? let updater: UpdaterProviding?
private let isPreview = ProcessInfo.processInfo.isPreview
init(state: AppState, updater: UpdaterProviding?, initialTab: SettingsTab? = nil) {
self.state = state
self.updater = updater
self._selectedTab = State(initialValue: initialTab ?? .general)
}
var body: some View { var body: some View {
TabView(selection: self.$selectedTab) { TabView(selection: self.$selectedTab) {
@@ -76,7 +83,10 @@ struct SettingsRootView: View {
self.updatePermissionMonitoring(for: newValue) self.updatePermissionMonitoring(for: newValue)
} }
.onDisappear { self.stopPermissionMonitoring() } .onDisappear { self.stopPermissionMonitoring() }
.task { await self.refreshPerms() } .task {
guard !self.isPreview else { return }
await self.refreshPerms()
}
} }
private func validTab(for requested: SettingsTab) -> SettingsTab { private func validTab(for requested: SettingsTab) -> SettingsTab {
@@ -86,10 +96,12 @@ struct SettingsRootView: View {
@MainActor @MainActor
private func refreshPerms() async { private func refreshPerms() async {
guard !self.isPreview else { return }
await self.permissionMonitor.refreshNow() await self.permissionMonitor.refreshNow()
} }
private func updatePermissionMonitoring(for tab: SettingsTab) { private func updatePermissionMonitoring(for tab: SettingsTab) {
guard !self.isPreview else { return }
let shouldMonitor = tab == .permissions let shouldMonitor = tab == .permissions
if shouldMonitor, !self.monitoringPermissions { if shouldMonitor, !self.monitoringPermissions {
self.monitoringPermissions = true self.monitoringPermissions = true
@@ -143,3 +155,15 @@ enum SettingsTabRouter {
extension Notification.Name { extension Notification.Name {
static let clawdisSelectSettingsTab = Notification.Name("clawdisSelectSettingsTab") static let clawdisSelectSettingsTab = Notification.Name("clawdisSelectSettingsTab")
} }
#if DEBUG
struct SettingsRootView_Previews: PreviewProvider {
static var previews: some View {
ForEach(SettingsTab.allCases, id: \.self) { tab in
SettingsRootView(state: .preview, updater: DisabledUpdaterController(), initialTab: tab)
.previewDisplayName(tab.title)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
}
#endif

View File

@@ -386,3 +386,12 @@ private enum ToolInstaller {
} }
} }
} }
#if DEBUG
struct ToolsSettings_Previews: PreviewProvider {
static var previews: some View {
ToolsSettings()
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -1,6 +1,12 @@
import AppKit import AppKit
import Foundation import Foundation
extension ProcessInfo {
var isPreview: Bool {
self.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
}
enum LaunchdManager { enum LaunchdManager {
private static func runLaunchctl(_ args: [String]) { private static func runLaunchctl(_ args: [String]) {
let process = Process() let process = Process()

View File

@@ -17,6 +17,7 @@ struct VoiceWakeSettings: View {
@State private var availableLocales: [Locale] = [] @State private var availableLocales: [Locale] = []
private let fieldLabelWidth: CGFloat = 120 private let fieldLabelWidth: CGFloat = 120
private let controlWidth: CGFloat = 240 private let controlWidth: CGFloat = 240
private let isPreview = ProcessInfo.processInfo.isPreview
private struct AudioInputDevice: Identifiable, Equatable { private struct AudioInputDevice: Identifiable, Equatable {
let uid: String let uid: String
@@ -83,13 +84,24 @@ struct VoiceWakeSettings: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12) .padding(.horizontal, 12)
} }
.task { await self.loadMicsIfNeeded() } .task {
.task { await self.loadLocalesIfNeeded() } guard !self.isPreview else { return }
.task { await self.restartMeter() } await self.loadMicsIfNeeded()
}
.task {
guard !self.isPreview else { return }
await self.loadLocalesIfNeeded()
}
.task {
guard !self.isPreview else { return }
await self.restartMeter()
}
.onChange(of: self.state.voiceWakeMicID) { _, _ in .onChange(of: self.state.voiceWakeMicID) { _, _ in
guard !self.isPreview else { return }
Task { await self.restartMeter() } Task { await self.restartMeter() }
} }
.onDisappear { .onDisappear {
guard !self.isPreview else { return }
Task { await self.meter.stop() } Task { await self.meter.stop() }
} }
} }
@@ -489,3 +501,12 @@ struct VoiceWakeSettings: View {
} }
} }
} }
#if DEBUG
struct VoiceWakeSettings_Previews: PreviewProvider {
static var previews: some View {
VoiceWakeSettings(state: .preview)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif