Skip to content

feat(settings): wire up functional settings#92

Draft
KafuChino123 wants to merge 7 commits intomasterfrom
feat/settings
Draft

feat(settings): wire up functional settings#92
KafuChino123 wants to merge 7 commits intomasterfrom
feat/settings

Conversation

@KafuChino123
Copy link
Copy Markdown
Contributor

Summary

Upgrade Settings window from UI-only placeholders to functional implementations across three batches.

Batch 1: General Settings

  • Terminal theme: TerminalAppearance.configure() now accepts a theme parameter (system/light/dark); terminal tabs read from @AppStorage
  • Auto update + channel: Sync autoUpdate to Sparkle's automaticallyChecksForUpdates; beta channel switches feed URL to appcast-beta.xml
  • Keep running on quit: When keepRunning && showInMenuBar, close windows instead of terminating — app stays in menu bar
  • External terminal: New ExternalTerminalLauncher opens Terminal.app/iTerm via AppleScript with DOCKER_HOST injected; added toolbar button in container terminal tab

Batch 2: System Settings

  • Pause on sleep: New SleepWakeManager listens for willSleepNotification/didWakeNotification, pauses all running containers on sleep and unpauses on wake
  • Docker context auto-switch: New DockerContextManager creates and switches to arcbox context on daemon startup, restores previous context on quit
  • Both toggles now use @AppStorage for persistence

Batch 3: Storage Settings

  • Time Machine exclusion: Toggle calls tmutil addexclusion/removeexclusion ~/.arcbox
  • Reset Docker Data: Confirmation dialog → stop all containers → prune containers/images/volumes/networks → post refresh notification
  • Reset All Data: Wraps Reset Docker with room for future expansion (K8s, machines)
  • Settings window now receives dockerClient environment

Not changed (requires backend gRPC support)

  • CPU/Memory limits, all Network settings, Storage Location, Rosetta, Admin Privileges, Kubernetes

New Files

  • ArcBox/Models/ExternalTerminalLauncher.swift
  • ArcBox/Models/SleepWakeManager.swift
  • ArcBox/Models/DockerContextManager.swift

Test Plan

  • Terminal theme: Switch between light/dark/system in Settings, verify terminal colors update
  • Auto update: Toggle and confirm Sparkle's automaticallyChecksForUpdates reflects the change
  • Update channel: Switch to beta, verify Sparkle feed URL changes
  • Keep running: Enable menu bar + keep running, Cmd+Q → app should remain in menu bar
  • External terminal: Click toolbar button in container terminal, verify Terminal.app/iTerm opens with DOCKER_HOST set
  • Pause on sleep: Close laptop lid, verify containers pause; open lid, verify they resume
  • Docker context: After app launch, run docker context show → should be arcbox; after quit → restored
  • Time Machine: Toggle and run tmutil isexcluded ~/.arcbox to verify
  • Reset Docker: Click button, confirm dialog, verify containers/images/volumes/networks are cleared

KafuChino123 and others added 7 commits March 4, 2026 23:40
Add Settings window (⌘,) with sidebar navigation and General pane.
Functional toggles: start at login, show in menu bar, keep running.
UI-only placeholders: updates and terminal sections.
Add Settings window (⌘,) with sidebar navigation and General pane.
Functional toggles: start at login, show in menu bar, keep running.
UI-only placeholders: updates and terminal sections.
…storage tabs

- Terminal theme: TerminalAppearance now accepts theme parameter (system/light/dark)
- Auto update: sync AppStorage to Sparkle's automaticallyChecksForUpdates
- Update channel: UpdaterDelegate switches feed URL for beta channel
- Keep running on quit: hide windows instead of terminating when menu bar is active
- External terminal: AppleScript launcher for Terminal.app/iTerm with DOCKER_HOST
- Pause on sleep: SleepWakeManager pauses/unpauses containers on macOS sleep/wake
- Docker context: auto-switch to arcbox context on startup, restore on quit
- Time Machine: toggle exclusion via tmutil for ~/.arcbox
- Reset Docker data: stop containers + prune all with confirmation dialog
Copilot AI review requested due to automatic review settings March 26, 2026 12:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Upgrades the macOS Settings experience from mostly placeholder UI to settings that persist and trigger real app behavior (Sparkle updates, terminal theming, container sleep/wake handling, Docker context switching, and storage maintenance actions).

Changes:

  • Adds a dedicated Settings window (with menu command) and introduces Settings tab views.
  • Wires settings to behavior: Sparkle auto-update/channel, terminal theme + external terminal launching, sleep/wake pause, Docker context switching.
  • Implements storage actions: Time Machine exclusion toggle + “Reset Docker Data / Reset All Data” operations (Docker prune + refresh notification).

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
arcbox-desktop-swift/Views/Settings/GeneralSettingsView.swift Adds General settings UI with @AppStorage keys (appears duplicated vs ArcBox/).
arcbox-desktop-swift/Views/Settings/NetworkSettingsView.swift Adds Network settings placeholder UI (appears duplicated vs ArcBox/).
arcbox-desktop-swift/Views/Settings/SettingsView.swift Adds settings navigation shell (duplicate tree concern).
arcbox-desktop-swift/Views/Settings/StorageSettingsView.swift Adds Storage settings placeholder UI (appears duplicated vs ArcBox/).
arcbox-desktop-swift/Views/Settings/SystemSettingsView.swift Adds System settings placeholder UI (appears duplicated vs ArcBox/).
ArcBox/ArcBoxApp.swift Adds Settings window + command, wires Sparkle prefs, sleep/wake manager, and Docker context switching; implements “keep running on quit”.
ArcBox/Models/DockerContextManager.swift Implements auto Docker context switching/restoration via ~/.docker/config.json + docker context create.
ArcBox/Models/ExternalTerminalLauncher.swift Adds AppleScript-based launcher for Terminal/iTerm with DOCKER_HOST injected.
ArcBox/Models/SleepWakeManager.swift Adds sleep/wake observers to pause/unpause running containers.
ArcBox/Theme/TerminalAppearance.swift Extends terminal appearance configuration to accept a theme preference (system/light/dark).
ArcBox/ViewModels/UpdaterDelegate.swift Makes Sparkle feed URL depend on user-selected update channel.
ArcBox/Views/Containers/Tabs/ContainerTerminalTab.swift Applies terminal theme preference and adds “Open in external terminal” button.
ArcBox/Views/Images/Tabs/ImageTerminalTab.swift Applies terminal theme preference to image terminal sessions.
ArcBox/Views/Settings/GeneralSettingsView.swift Adds persisted General settings (login item, update prefs, terminal prefs).
ArcBox/Views/Settings/NetworkSettingsView.swift Adds Network settings UI (currently placeholder behavior).
ArcBox/Views/Settings/SettingsView.swift Adds settings tab navigation and layout.
ArcBox/Views/Settings/StorageSettingsView.swift Implements Time Machine toggle + reset Docker/all data flows with progress/result messaging.
ArcBox/Views/Settings/SystemSettingsView.swift Adds System settings UI, persisting some toggles (others remain placeholders).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +3 to +67
// MARK: - Settings Tab Enum

enum SettingsTab: String, CaseIterable, Identifiable {
case general = "General"
case system = "System"
case network = "Network"
case storage = "Storage"
case machines = "Machines"
case docker = "Docker"
case kubernetes = "Kubernetes"

var id: String { rawValue }

var sfSymbol: String {
switch self {
case .general: return "gearshape"
case .system: return "square.grid.2x2"
case .network: return "globe"
case .storage: return "externaldrive"
case .machines: return "desktopcomputer"
case .docker: return "shippingbox"
case .kubernetes: return "helm"
}
}
}

// MARK: - Settings View

struct SettingsView: View {
@State private var selectedTab: SettingsTab = .general

var body: some View {
NavigationSplitView {
List(SettingsTab.allCases, selection: $selectedTab) { tab in
Label(tab.rawValue, systemImage: tab.sfSymbol)
.tag(tab)
}
.listStyle(.sidebar)
.navigationSplitViewColumnWidth(min: 150, ideal: 180, max: 220)
} detail: {
settingsContent
.navigationTitle(selectedTab.rawValue)
}
.toolbar(removing: .sidebarToggle)
.frame(width: 700, height: 580)
}

@ViewBuilder
private var settingsContent: some View {
switch selectedTab {
case .general:
GeneralSettingsView()
case .system:
SystemSettingsView()
case .network:
NetworkSettingsView()
case .storage:
StorageSettingsView()
default:
Text(selectedTab.rawValue)
.font(.title2)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new Settings views are duplicated under arcbox-desktop-swift/, but the repository’s active Xcode project is ArcBox.xcodeproj and does not reference arcbox-desktop-swift (no project.pbxproj matches). Keeping a parallel Settings implementation in an apparently unused tree is likely to become stale/confusing; consider removing this duplicate tree or ensuring only one source of truth is built.

Suggested change
// MARK: - Settings Tab Enum
enum SettingsTab: String, CaseIterable, Identifiable {
case general = "General"
case system = "System"
case network = "Network"
case storage = "Storage"
case machines = "Machines"
case docker = "Docker"
case kubernetes = "Kubernetes"
var id: String { rawValue }
var sfSymbol: String {
switch self {
case .general: return "gearshape"
case .system: return "square.grid.2x2"
case .network: return "globe"
case .storage: return "externaldrive"
case .machines: return "desktopcomputer"
case .docker: return "shippingbox"
case .kubernetes: return "helm"
}
}
}
// MARK: - Settings View
struct SettingsView: View {
@State private var selectedTab: SettingsTab = .general
var body: some View {
NavigationSplitView {
List(SettingsTab.allCases, selection: $selectedTab) { tab in
Label(tab.rawValue, systemImage: tab.sfSymbol)
.tag(tab)
}
.listStyle(.sidebar)
.navigationSplitViewColumnWidth(min: 150, ideal: 180, max: 220)
} detail: {
settingsContent
.navigationTitle(selectedTab.rawValue)
}
.toolbar(removing: .sidebarToggle)
.frame(width: 700, height: 580)
}
@ViewBuilder
private var settingsContent: some View {
switch selectedTab {
case .general:
GeneralSettingsView()
case .system:
SystemSettingsView()
case .network:
NetworkSettingsView()
case .storage:
StorageSettingsView()
default:
Text(selectedTab.rawValue)
.font(.title2)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
/// Deprecated duplicate Settings implementation.
///
/// This `SettingsView` lives under `arcbox-desktop-swift/` and is not referenced
/// from the active `ArcBox.xcodeproj`. It is kept only for historical/contextual
/// purposes. Do not add new behavior here; instead, update the main Settings
/// implementation used by the app.
@available(*, deprecated, message: "Use the main SettingsView from ArcBox.xcodeproj instead of the duplicate in arcbox-desktop-swift.")
struct SettingsView: View {
var body: some View {
Text(
"""
This SettingsView is defined in arcbox-desktop-swift and is deprecated.
It is not used by the main ArcBox.xcodeproj target.
Please update and reference the primary Settings implementation instead.
"""
)
.multilineTextAlignment(.center)
.padding()
}

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +15
if channel == "beta" {
// Replace the appcast filename to point to the beta feed.
// e.g. ".../appcast.xml" → ".../appcast-beta.xml"
return baseURL.replacingOccurrences(of: "appcast.xml", with: "appcast-beta.xml")
}
return baseURL
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update channel switching likely won't work with the current replacement logic. CI/release packaging sets SUFeedURL to .../desktop/appcast/<channel>.xml (e.g. stable.xml/beta.xml), but this code only replaces appcast.xmlappcast-beta.xml, which won't match those URLs. Consider parsing the URL and swapping the last path component based on updateChannel (or replacing stable.xmlbeta.xml).

Suggested change
if channel == "beta" {
// Replace the appcast filename to point to the beta feed.
// e.g. ".../appcast.xml" → ".../appcast-beta.xml"
return baseURL.replacingOccurrences(of: "appcast.xml", with: "appcast-beta.xml")
}
return baseURL
// Attempt to adjust the feed URL based on the selected update channel.
// CI/release packaging sets SUFeedURL to ".../desktop/appcast/<channel>.xml"
// (e.g. "stable.xml" / "beta.xml"), but we also support the older
// "appcast.xml" / "appcast-beta.xml" pattern.
guard let url = URL(string: baseURL) else {
return baseURL
}
let currentFilename = url.lastPathComponent
var targetFilename = currentFilename
switch channel {
case "beta":
// Prefer switching "stable.xml" → "beta.xml"
if currentFilename == "stable.xml" {
targetFilename = "beta.xml"
} else if currentFilename == "appcast.xml" {
// Fallback for older "appcast.xml" → "appcast-beta.xml" scheme
targetFilename = "appcast-beta.xml"
}
case "stable":
// Ensure we point back to the stable feed when requested.
if currentFilename == "beta.xml" {
targetFilename = "stable.xml"
} else if currentFilename == "appcast-beta.xml" {
targetFilename = "appcast.xml"
}
default:
// For any other channel, leave the URL unchanged.
break
}
// If no change was needed, return the original baseURL.
if targetFilename == currentFilename {
return baseURL
}
let basePathURL = url.deletingLastPathComponent()
let updatedURL = basePathURL.appendingPathComponent(targetFilename)
return updatedURL.absoluteString

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +126
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/usr/bin/tmutil")
proc.arguments = include ? ["removeexclusion", path] : ["addexclusion", path]
proc.standardOutput = FileHandle.nullDevice
proc.standardError = FileHandle.nullDevice
do {
try proc.run()
proc.waitUntilExit()
} catch {
// tmutil may require admin privileges — silently fail
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateTimeMachineExclusion(include:) runs tmutil synchronously and calls waitUntilExit() on the main actor, which can block the UI if tmutil hangs/prompts. Consider running this Process work off the main actor (e.g. in a detached Task) and reporting failure (exit status / thrown error) back to the UI.

Suggested change
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/usr/bin/tmutil")
proc.arguments = include ? ["removeexclusion", path] : ["addexclusion", path]
proc.standardOutput = FileHandle.nullDevice
proc.standardError = FileHandle.nullDevice
do {
try proc.run()
proc.waitUntilExit()
} catch {
// tmutil may require admin privileges — silently fail
Task.detached {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/usr/bin/tmutil")
proc.arguments = include ? ["removeexclusion", path] : ["addexclusion", path]
proc.standardOutput = FileHandle.nullDevice
proc.standardError = FileHandle.nullDevice
do {
try proc.run()
proc.waitUntilExit()
} catch {
// tmutil may require admin privileges — silently fail
}

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +41
func start() {
let workspace = NSWorkspace.shared.notificationCenter
sleepObserver = workspace.addObserver(
forName: NSWorkspace.willSleepNotification, object: nil, queue: .main
) { [weak self] _ in
guard let self else { return }
Task { @MainActor in
await self.handleSleep()
}
}
wakeObserver = workspace.addObserver(
forName: NSWorkspace.didWakeNotification, object: nil, queue: .main
) { [weak self] _ in
guard let self else { return }
Task { @MainActor in
await self.handleWake()
}
}
logger.info("Sleep/wake monitoring started")
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start() unconditionally adds sleep/wake observers without checking if monitoring is already active. If start() is called more than once without stop(), notifications will be handled multiple times (duplicate pauses/unpauses). Consider making start() idempotent (e.g. guard observers are nil, or call stop() before re-adding).

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +76
@State private var memoryLimit: Double = 9
@State private var cpuLimit: Double = 17 // 17 = "None" (beyond max)
@State private var useAdminPrivileges = true
@AppStorage("switchDockerContextAutomatically") private var switchContextAutomatically = true
@State private var useRosetta = true
@AppStorage("pauseContainersWhileSleeping") private var pauseContainersWhileSleeping = true

private let memoryRange: ClosedRange<Double> = 1...14
private let cpuSteps: [String] = ["100%", ""] // display only

var body: some View {
Form {
Section {
Text("Resources are only used as needed. These are limits, not reservations. [Learn more](#)")
.font(.callout)
.foregroundStyle(.secondary)

LabeledContent {
HStack {
Text("1 GiB")
.font(.caption)
.foregroundStyle(.secondary)
Slider(value: $memoryLimit, in: memoryRange, step: 1)
Text("14 GiB")
.font(.caption)
.foregroundStyle(.secondary)
}
} label: {
VStack(alignment: .leading, spacing: 2) {
Text("Memory limit")
Text("\(Int(memoryLimit)) GiB")
.font(.caption)
.foregroundStyle(.secondary)
}
}

LabeledContent {
HStack {
Text("100%")
.font(.caption)
.foregroundStyle(.secondary)
Slider(value: $cpuLimit, in: 1...17, step: 1)
Text("None")
.font(.caption)
.foregroundStyle(.secondary)
}
} label: {
VStack(alignment: .leading, spacing: 2) {
Text("CPU limit")
Text(cpuLimitLabel)
.font(.caption)
.foregroundStyle(.secondary)
}
}
} header: {
Text("Resources")
}

Section("Environment") {
LabeledContent {
Toggle("", isOn: $useAdminPrivileges)
.labelsHidden()
} label: {
VStack(alignment: .leading, spacing: 2) {
Text("Use admin privileges for enhanced features")
Text("This can improve performance and compatibility. [Learn more](#)")
.font(.caption)
.foregroundStyle(.secondary)
}
}

Toggle("Switch Docker & Kubernetes context automatically", isOn: $switchContextAutomatically)
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This view presents interactive resource/environment controls (sliders/toggles) but they are backed only by @State and are not applied or persisted. Given the PR description notes CPU/Memory/Admin/Rosetta require backend support, consider disabling these controls (or clearly marking them as not yet supported) to avoid misleading users.

Copilot uses AI. Check for mistakes.
@AppStorage("pauseContainersWhileSleeping") private var pauseContainersWhileSleeping = true

private let memoryRange: ClosedRange<Double> = 1...14
private let cpuSteps: [String] = ["100%", ""] // display only
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cpuSteps is declared but unused, which will produce a compiler warning and adds noise for future maintenance. Consider removing it or wiring it into the UI (e.g. tick labels) if intended.

Suggested change
private let cpuSteps: [String] = ["100%", ""] // display only

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +88
/// Creates the arcbox context in Docker's context meta store.
private static func createArcBoxContext() {
// Use docker CLI to create the context instead of manual file manipulation
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/usr/bin/env")
proc.arguments = [
"docker", "context", "create", "arcbox",
"--docker", "host=\(arcboxSocketPath)",
"--description", "ArcBox Desktop",
]
proc.standardOutput = FileHandle.nullDevice
proc.standardError = FileHandle.nullDevice
// Silence errors if context already exists
do {
try proc.run()
proc.waitUntilExit()
} catch {
// Context may already exist — that's fine
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createArcBoxContext() runs docker context create ... synchronously and calls waitUntilExit(). With the target’s default MainActor isolation, calling switchToArcBox() from UI-driven code can block the main thread if docker is slow/hung. Consider moving the Process execution off the main actor (or making these APIs async) so startup/UI remains responsive.

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +155
for container in running {
guard let id = container.Id else { continue }
_ = try? await docker.api.ContainerStop(path: .init(id: id))
}

// Prune everything: containers, images, volumes, networks
_ = try? await docker.api.ContainerPrune()
_ = try? await docker.api.ImagePrune()
_ = try? await docker.api.NetworkPrune()
_ = try? await docker.api.VolumePrune()

resetResultMessage = "Docker data has been reset successfully."
NotificationCenter.default.post(name: .dockerDataChanged, object: nil)
} catch {
resetResultMessage = "Reset failed: \(error.localizedDescription)"
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resetDockerData() uses try? for stop/prune calls, so failures will be silently ignored and the UI will still show a success message. This can lead to false positives (e.g. data not actually pruned). Consider handling and surfacing errors from each operation (or at least failing the overall reset if any prune fails).

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +101
private static func readConfig() throws -> [String: Any] {
let url = URL(fileURLWithPath: configPath)
guard FileManager.default.fileExists(atPath: configPath) else {
return [:]
}
let data = try Data(contentsOf: url)
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return [:]
}
return json
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readConfig() returns an empty dictionary when the JSON parses but isn't a [String: Any] root object. In that case switchToArcBox() will proceed to write a new config containing only currentContext, potentially clobbering a user's existing Docker config. Consider throwing on unexpected JSON shape (or bailing out without writing) to avoid data loss.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +76
struct NetworkSettingsView: View {
@State private var proxyMode: ProxyMode = .auto
@State private var allowContainerDomains = true
@State private var enableHTTPS = true
@State private var ipRange = "192.168.138.0/23"

private let ipRangeOptions = [
"192.168.138.0/23",
"172.16.0.0/12",
"10.0.0.0/8",
]

var body: some View {
Form {
Section("Proxy") {
VStack(alignment: .leading, spacing: 4) {
Text("Apply an HTTP, HTTPS, or SOCKS proxy to all traffic from containers and machines.")
.font(.callout)
.foregroundStyle(.secondary)
.padding(.bottom, 4)

Picker("", selection: $proxyMode) {
ForEach(ProxyMode.allCases) { mode in
Text(mode.rawValue).tag(mode)
}
}
.pickerStyle(.radioGroup)
.labelsHidden()
}
}

Section("Domains") {
LabeledContent {
Toggle("", isOn: $allowContainerDomains)
.labelsHidden()
} label: {
VStack(alignment: .leading, spacing: 2) {
Text("Allow access to container domains & IPs")
Text("Use domains and IPs to connect to containers and machines without port forwarding. [Learn more](#)")
.font(.caption)
.foregroundStyle(.secondary)
}
}

Toggle("Enable HTTPS for container domains", isOn: $enableHTTPS)

LabeledContent {
Picker("", selection: $ipRange) {
ForEach(ipRangeOptions, id: \.self) { option in
if option == "192.168.138.0/23" {
Text("\(option) (default)").tag(option)
} else {
Text(option).tag(option)
}
}
}
.labelsHidden()
.frame(width: 220)
} label: {
VStack(alignment: .leading, spacing: 2) {
Text("IP range")
Text("Used for domains and machines. Containers and Kubernetes use different IPs. Don't change this unless you run into issues with the default.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Network settings in this view are currently all @State-backed and not applied/persisted, but the Settings UI exposes them as if functional. Since the PR description says network settings require backend support, consider disabling these controls or adding explicit "not yet supported" messaging so users don't think the toggles have effect.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants