macOS: add settings previews
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -5,99 +5,116 @@ import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class AppState: ObservableObject {
|
||||
private let isPreview: Bool
|
||||
|
||||
private func ifNotPreview(_ action: () -> Void) {
|
||||
guard !self.isPreview else { return }
|
||||
action()
|
||||
}
|
||||
|
||||
enum ConnectionMode: String {
|
||||
case local
|
||||
case remote
|
||||
}
|
||||
|
||||
@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 {
|
||||
didSet { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } }
|
||||
didSet { self.ifNotPreview { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } } }
|
||||
}
|
||||
|
||||
@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 {
|
||||
didSet { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } }
|
||||
}
|
||||
|
||||
@Published var swabbleEnabled: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey)
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey)
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var swabbleTriggerWords: [String] {
|
||||
didSet {
|
||||
// Preserve the raw editing state; sanitization happens when we actually use the triggers.
|
||||
UserDefaults.standard.set(self.swabbleTriggerWords, forKey: swabbleTriggersKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.swabbleTriggerWords, forKey: swabbleTriggersKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var voiceWakeTriggerChime: VoiceWakeChime {
|
||||
didSet { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) }
|
||||
didSet { self.ifNotPreview { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) } }
|
||||
}
|
||||
|
||||
@Published var voiceWakeSendChime: VoiceWakeChime {
|
||||
didSet { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) }
|
||||
didSet { self.ifNotPreview { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) } }
|
||||
}
|
||||
|
||||
@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 {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey)
|
||||
AppActivationPolicy.apply(showDockIcon: self.showDockIcon)
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey)
|
||||
AppActivationPolicy.apply(showDockIcon: self.showDockIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var voiceWakeMicID: String {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var voiceWakeLocaleID: String {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey)
|
||||
if self.swabbleEnabled {
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
didSet { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) } }
|
||||
}
|
||||
|
||||
@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 {
|
||||
didSet { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } }
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -106,38 +123,41 @@ final class AppState: ObservableObject {
|
||||
@Published var sendCelebrationTick: Int = 0
|
||||
@Published var heartbeatsEnabled: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey)
|
||||
Task { _ = await AgentRPC.shared.setHeartbeatsEnabled(self.heartbeatsEnabled) }
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey)
|
||||
Task { _ = await AgentRPC.shared.setHeartbeatsEnabled(self.heartbeatsEnabled) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
didSet { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) } }
|
||||
}
|
||||
|
||||
@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 {
|
||||
didSet { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) } }
|
||||
}
|
||||
|
||||
@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 {
|
||||
didSet { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) } }
|
||||
}
|
||||
|
||||
private var earBoostTask: Task<Void, Never>?
|
||||
|
||||
init() {
|
||||
init(preview: Bool = false) {
|
||||
self.isPreview = preview
|
||||
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
|
||||
self.launchAtLogin = false
|
||||
self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen")
|
||||
@@ -199,16 +219,20 @@ final class AppState: ObservableObject {
|
||||
let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey)
|
||||
self.webChatPort = storedPort > 0 ? storedPort : 18788
|
||||
|
||||
Task.detached(priority: .utility) { [weak self] in
|
||||
let current = await LaunchAgentManager.status()
|
||||
await MainActor.run { [weak self] in self?.launchAtLogin = current }
|
||||
if !self.isPreview {
|
||||
Task.detached(priority: .utility) { [weak self] in
|
||||
let current = await LaunchAgentManager.status()
|
||||
await MainActor.run { [weak self] in self?.launchAtLogin = current }
|
||||
}
|
||||
}
|
||||
|
||||
if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() {
|
||||
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) {
|
||||
@@ -243,14 +267,15 @@ final class AppState: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
self.swabbleEnabled = enabled
|
||||
guard !self.isPreview else { return }
|
||||
|
||||
if !enabled {
|
||||
self.swabbleEnabled = false
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
return
|
||||
}
|
||||
|
||||
if PermissionManager.voiceWakePermissionsGranted() {
|
||||
self.swabbleEnabled = true
|
||||
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
|
||||
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
|
||||
enum AppStateStore {
|
||||
static let shared = AppState()
|
||||
|
||||
@@ -2,6 +2,7 @@ import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct ConfigSettings: View {
|
||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||
@State private var configModel: String = ""
|
||||
@State private var customModel: String = ""
|
||||
@State private var configSaving = false
|
||||
@@ -131,6 +132,7 @@ struct ConfigSettings: View {
|
||||
}
|
||||
.task {
|
||||
guard !self.hasLoaded else { return }
|
||||
guard !self.isPreview else { return }
|
||||
self.hasLoaded = true
|
||||
self.loadConfig()
|
||||
await self.loadModels()
|
||||
@@ -247,3 +249,12 @@ struct ConfigSettings: View {
|
||||
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
|
||||
|
||||
@@ -36,7 +36,7 @@ enum DebugActions {
|
||||
_ = 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 = """
|
||||
This is a debug test from the Mac app. Reply with "Debug test works (and a funny pun)" \
|
||||
if you received that.
|
||||
@@ -51,7 +51,7 @@ enum DebugActions {
|
||||
return .success("Forwarded. Await reply.")
|
||||
case let .failure(error):
|
||||
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)
|
||||
? reason!
|
||||
: "No error returned. Check /tmp/clawdis.log or rpc output."
|
||||
return .failure("Local send failed: \(detail)")
|
||||
return .failure(.message("Local send failed: \(detail)"))
|
||||
}
|
||||
} catch {
|
||||
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()
|
||||
df.calendar = Calendar(identifier: .iso8601)
|
||||
df.locale = Locale(identifier: "en_US_POSIX")
|
||||
@@ -102,3 +102,13 @@ enum DebugActions {
|
||||
return "/tmp/clawdis.log"
|
||||
}
|
||||
}
|
||||
|
||||
enum DebugActionError: LocalizedError {
|
||||
case message(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case let .message(text): text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct DebugSettings: View {
|
||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
|
||||
@AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0
|
||||
@AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue
|
||||
@@ -44,9 +45,9 @@ struct DebugSettings: View {
|
||||
}
|
||||
LabeledContent("PID") { Text("\(ProcessInfo.processInfo.processIdentifier)") }
|
||||
LabeledContent("Log file") {
|
||||
Button("Open pino log") { self.openLog() }
|
||||
.help(self.pinoLogPath)
|
||||
Text(self.pinoLogPath)
|
||||
Button("Open pino log") { DebugActions.openLog() }
|
||||
.help(DebugActions.pinoLogPath())
|
||||
Text(DebugActions.pinoLogPath())
|
||||
.font(.caption2.monospaced())
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
@@ -205,6 +206,7 @@ struct DebugSettings: View {
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.task {
|
||||
guard !self.isPreview else { return }
|
||||
await self.reloadModels()
|
||||
self.loadSessionStorePath()
|
||||
}
|
||||
@@ -256,9 +258,9 @@ struct DebugSettings: View {
|
||||
case let .success(message):
|
||||
self.debugSendStatus = message
|
||||
self.debugSendError = nil
|
||||
case let .failure(message):
|
||||
self.debugSendStatus = message
|
||||
self.debugSendError = nil
|
||||
case let .failure(error):
|
||||
self.debugSendStatus = nil
|
||||
self.debugSendError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,3 +350,12 @@ struct DebugSettings: View {
|
||||
.appendingPathComponent("clawdis.json")
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct DebugSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DebugSettings()
|
||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -238,7 +238,7 @@ private actor GatewayChannelActor {
|
||||
if let urlError = error as? URLError {
|
||||
let desc = urlError.localizedDescription.isEmpty ? "cancelled" : urlError.localizedDescription
|
||||
return NSError(
|
||||
domain: urlError.errorDomain,
|
||||
domain: URLError.errorDomain,
|
||||
code: urlError.errorCode,
|
||||
userInfo: [NSLocalizedDescriptionKey: "\(context): \(desc)"])
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ struct GeneralSettings: View {
|
||||
@State private var relayInstalling = false
|
||||
@State private var remoteStatus: RemoteStatus = .idle
|
||||
@State private var showRemoteAdvanced = false
|
||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical) {
|
||||
@@ -65,6 +66,7 @@ struct GeneralSettings: View {
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.onAppear {
|
||||
guard !self.isPreview else { return }
|
||||
self.refreshCLIStatus()
|
||||
self.refreshRelayStatus()
|
||||
}
|
||||
@@ -527,3 +529,12 @@ private func healthAgeString(_ ms: Double?) -> String {
|
||||
guard let ms else { return "unknown" }
|
||||
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
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
struct InstancesSettings: View {
|
||||
@StateObject private var store = InstancesStore.shared
|
||||
@ObservedObject var store: InstancesStore
|
||||
|
||||
init(store: InstancesStore = .shared) {
|
||||
self.store = store
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -87,3 +91,12 @@ struct InstancesSettings: View {
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct InstancesSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InstancesSettings(store: .preview())
|
||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -28,6 +28,7 @@ struct InstanceInfo: Identifiable, Codable {
|
||||
@MainActor
|
||||
final class InstancesStore: ObservableObject {
|
||||
static let shared = InstancesStore()
|
||||
let isPreview: Bool
|
||||
|
||||
@Published var instances: [InstanceInfo] = []
|
||||
@Published var lastError: String?
|
||||
@@ -39,7 +40,12 @@ final class InstancesStore: ObservableObject {
|
||||
private let interval: TimeInterval = 30
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
|
||||
init(isPreview: Bool = false) {
|
||||
self.isPreview = isPreview
|
||||
}
|
||||
|
||||
func start() {
|
||||
guard !self.isPreview else { return }
|
||||
guard self.task == nil else { return }
|
||||
self.observeGatewayEvents()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,6 +95,49 @@ struct SessionDefaults {
|
||||
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 {
|
||||
let id: String
|
||||
let name: String
|
||||
|
||||
@@ -3,13 +3,23 @@ import SwiftUI
|
||||
|
||||
@MainActor
|
||||
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 lastLoaded: Date?
|
||||
@State private var errorMessage: String?
|
||||
@State private var loading = 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 {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
self.header
|
||||
@@ -22,6 +32,7 @@ struct SessionsSettings: View {
|
||||
.padding(.horizontal, 12)
|
||||
.task {
|
||||
guard !self.hasLoaded else { return }
|
||||
guard !self.isPreview else { return }
|
||||
self.hasLoaded = true
|
||||
await self.refresh()
|
||||
}
|
||||
@@ -149,6 +160,7 @@ struct SessionsSettings: View {
|
||||
|
||||
private func refresh() async {
|
||||
guard !self.loading else { return }
|
||||
guard !self.isPreview else { return }
|
||||
self.loading = true
|
||||
self.errorMessage = nil
|
||||
|
||||
@@ -209,3 +221,12 @@ private struct Badge: View {
|
||||
.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
|
||||
|
||||
@@ -6,6 +6,13 @@ struct SettingsRootView: View {
|
||||
@State private var monitoringPermissions = false
|
||||
@State private var selectedTab: SettingsTab = .general
|
||||
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 {
|
||||
TabView(selection: self.$selectedTab) {
|
||||
@@ -76,7 +83,10 @@ struct SettingsRootView: View {
|
||||
self.updatePermissionMonitoring(for: newValue)
|
||||
}
|
||||
.onDisappear { self.stopPermissionMonitoring() }
|
||||
.task { await self.refreshPerms() }
|
||||
.task {
|
||||
guard !self.isPreview else { return }
|
||||
await self.refreshPerms()
|
||||
}
|
||||
}
|
||||
|
||||
private func validTab(for requested: SettingsTab) -> SettingsTab {
|
||||
@@ -86,10 +96,12 @@ struct SettingsRootView: View {
|
||||
|
||||
@MainActor
|
||||
private func refreshPerms() async {
|
||||
guard !self.isPreview else { return }
|
||||
await self.permissionMonitor.refreshNow()
|
||||
}
|
||||
|
||||
private func updatePermissionMonitoring(for tab: SettingsTab) {
|
||||
guard !self.isPreview else { return }
|
||||
let shouldMonitor = tab == .permissions
|
||||
if shouldMonitor, !self.monitoringPermissions {
|
||||
self.monitoringPermissions = true
|
||||
@@ -143,3 +155,15 @@ enum SettingsTabRouter {
|
||||
extension Notification.Name {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
extension ProcessInfo {
|
||||
var isPreview: Bool {
|
||||
self.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||
}
|
||||
}
|
||||
|
||||
enum LaunchdManager {
|
||||
private static func runLaunchctl(_ args: [String]) {
|
||||
let process = Process()
|
||||
|
||||
@@ -17,6 +17,7 @@ struct VoiceWakeSettings: View {
|
||||
@State private var availableLocales: [Locale] = []
|
||||
private let fieldLabelWidth: CGFloat = 120
|
||||
private let controlWidth: CGFloat = 240
|
||||
private let isPreview = ProcessInfo.processInfo.isPreview
|
||||
|
||||
private struct AudioInputDevice: Identifiable, Equatable {
|
||||
let uid: String
|
||||
@@ -83,13 +84,24 @@ struct VoiceWakeSettings: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
.task { await self.loadMicsIfNeeded() }
|
||||
.task { await self.loadLocalesIfNeeded() }
|
||||
.task { await self.restartMeter() }
|
||||
.task {
|
||||
guard !self.isPreview else { return }
|
||||
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
|
||||
guard !self.isPreview else { return }
|
||||
Task { await self.restartMeter() }
|
||||
}
|
||||
.onDisappear {
|
||||
guard !self.isPreview else { return }
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user