import SwiftUI
import AppKit
import UniformTypeIdentifiers
import NucleicCore
import NucleicProtocol

/// App settings. A source-list sidebar groups the panes into sections — Interface, Agents & AI,
/// Environments, Connectivity — so the panel stays legible and keeps scaling as areas mature. It
/// replaces the original horizontal tab bar, which had run out of room at nine tabs (and dated from
/// when there were only a few). Each pane is a self-contained `Form`; the sidebar just picks which
/// one the detail column shows, and the choice is remembered across launches.
struct SettingsView: View {
    @AppStorage(AppAppearance.storageKey) private var appearanceRaw = AppAppearance.dark.rawValue
    @AppStorage(AppTextSize.storageKey) private var textSizeRaw = AppTextSize.medium.rawValue
    /// Persists the last-open pane so reopening Settings returns you where you left off.
    @AppStorage("nucleic.settings.selectedPane") private var storedPane = SettingsPane.general.rawValue

    /// Starts unset so the first `onAppear` assignment restores `storedPane` — resolving it inline in
    /// the initializer would fight the sidebar's selection binding on first render.
    @State private var selection: SettingsPane?

    /// Pins the sidebar open: sets `canCollapse = false` on the AppKit split-view item so the column
    /// never collapses on its own when the window narrows (see `SidebarColumnController`). Paired with
    /// `.toolbar(removing: .sidebarToggle)` below, which drops the manual collapse button — together
    /// they make the sidebar impossible to hide.
    // The main-app sidebar is 306pt; Settings needs 20% less width (244.8pt).
    @State private var sidebarColumn = SidebarColumnController(
        fixedWidth: SidebarColumnController.defaultFixedWidth * 0.8)

    private var appearance: AppAppearance { AppAppearance(rawValue: appearanceRaw) ?? .dark }
    private var textSize: AppTextSize { AppTextSize(rawValue: textSizeRaw) ?? .medium }

    var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                // Emit each Section explicitly rather than through a `ForEach` over the groups:
                // on macOS a sidebar `List` whose top-level `ForEach` yields `Section`s drops
                // alternating sections, so the odd groups (Agents & AI, Connectivity) rendered as
                // empty headers. Spelling out the sections sidesteps that; adding a future group is
                // still a one-liner here plus a case in `SettingsGroup`.
                sidebarSection(.interface)
                sidebarSection(.agents)
                sidebarSection(.environments)
                sidebarSection(.connectivity)
            }
            // Fixed sidebar width: 20% narrower than the main-app sidebar. Equal min/ideal/max sets the
            // SwiftUI hint; the hard pin (non-draggable divider) is enforced in AppKit via
            // `sidebarColumn.fixedWidth`.
            .navigationSplitViewColumnWidth(
                min: sidebarColumn.fixedWidth,
                ideal: sidebarColumn.fixedWidth,
                max: sidebarColumn.fixedWidth)
            // Settings should always show its sidebar: drop the manual collapse control, and pin the
            // AppKit split-view item open so a narrow window can't auto-collapse it either.
            .toolbar(removing: .sidebarToggle)
            .background { SidebarColumnConfigurator(controller: sidebarColumn) }
        } detail: {
            detailPane
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                // Match the main window's detail surface. Each pane is a grouped `Form`, whose
                // default scroll background is the system window gray (a warmer, lighter tone than
                // our theme). Hide that and paint the app's own `AppTheme.background` so the Settings
                // pane reads identically to the normal window's detail column (RootView paints the
                // same fill there).
                .scrollContentBackground(.hidden)
                .background(AppTheme.background)
        }
        .navigationSplitViewStyle(.balanced)
        // Fixed overall width so the settings window can't be resized narrow (which is what
        // drove the sidebar's auto-collapse). The narrower sidebar leaves more room for detail. Height stays
        // flexible.
        .frame(minWidth: 840, idealWidth: 840, maxWidth: 840, minHeight: 560, maxHeight: .infinity)
        .preferredColorScheme(appearance.colorScheme)
        .dynamicTypeSize(textSize.dynamicTypeSize)
        .background(SettingsWindowConfigurator(reapplyToken: (selection ?? .general).rawValue))
        .onAppear { if selection == nil { selection = SettingsPane(rawValue: storedPane) ?? .general } }
        .onChange(of: selection) { _, pane in if let pane { storedPane = pane.rawValue } }
    }

    /// One titled sidebar section listing its group's panes as selectable rows.
    private func sidebarSection(_ group: SettingsGroup) -> some View {
        Section(group.title) {
            ForEach(group.panes) { pane in
                Label(pane.title, systemImage: pane.symbol).tag(pane)
            }
        }
    }

    /// The content column for the current selection. Each case is an existing `Form`-based pane view.
    @ViewBuilder private var detailPane: some View {
        switch selection ?? .general {
        case .general: GeneralSettingsTab()
        case .appearance: AppearanceSettingsTab()
        case .notifications: NotificationSettingsTab()
        case .chats: ChatSettingsTab()
        case .agents: AgentsSettingsTab()
        case .usage: UsageSettingsTab()
        case .intelligence: IntelligenceSettingsTab()
        case .sandbox: SandboxSettingsTab()
        case .virtualMachines: MacVMSettingsTab()
        case .control: ControlSettingsTab()
        case .git: GitSettingsTab()
        case .antimatter: RemoteSettingsTab()
        }
    }
}

/// One selectable pane in the Settings sidebar. Raw values are stable (they back the persisted
/// last-open pane) and need not be contiguous, so panes can be reordered or inserted freely.
private enum SettingsPane: Int, Identifiable, CaseIterable, Hashable {
    case general = 0
    case appearance = 1
    case notifications = 2
    case chats = 3
    case agents = 4
    case intelligence = 5
    case sandbox = 6
    case virtualMachines = 7
    case control = 8
    case git = 9
    case antimatter = 10
    case usage = 11

    var id: Int { rawValue }

    var title: String {
        switch self {
        case .general: "General"
        case .appearance: "Appearance"
        case .notifications: "Notifications"
        case .chats: "Chats"
        case .agents: "Agents"
        case .intelligence: "Intelligence"
        case .sandbox: "Sandbox"
        case .virtualMachines: "Virtual Machines"
        case .control: "Control"
        case .git: "Git"
        case .antimatter: "Antimatter"
        case .usage: "Usage"
        }
    }

    var symbol: String {
        switch self {
        case .general: "gearshape"
        case .appearance: "paintbrush"
        case .notifications: "bell.badge"
        case .chats: "bubble.left.and.bubble.right"
        case .agents: "cpu"
        case .intelligence: "sparkles"
        case .sandbox: "shippingbox"
        case .virtualMachines: "desktopcomputer"
        case .control: "lock.shield"
        case .git: "arrow.triangle.branch"
        case .antimatter: "network"
        case .usage: "gauge.with.dots.needle.67percent"
        }
    }
}

/// A titled section in the sidebar. Declaration order here is the on-screen order of both the
/// sections and the panes within them.
private enum SettingsGroup: Int, Identifiable, CaseIterable {
    case interface
    case agents
    case environments
    case connectivity

    var id: Int { rawValue }

    var title: String {
        switch self {
        case .interface: "Interface"
        case .agents: "Agents & AI"
        case .environments: "Environments"
        case .connectivity: "Connectivity"
        }
    }

    var panes: [SettingsPane] {
        switch self {
        case .interface: [.general, .appearance, .notifications, .chats]
        case .agents: [.agents, .usage, .intelligence]
        case .environments: [.sandbox, .virtualMachines, .control]
        case .connectivity: [.git, .antimatter]
        }
    }
}

/// Pins the Settings window's chrome that SwiftUI's `Settings` scene otherwise
/// manages on its own: a constant "Nucleic Settings" title (the default would
/// track the selected tab), float-above-everything level so the main window can't
/// cover it, and a vertically resizable frame.
private struct SettingsWindowConfigurator: NSViewRepresentable {
    /// Re-runs `updateNSView` whenever the selected tab changes so we can restore
    /// the title after the tab switch overwrites it.
    var reapplyToken: Int?

    func makeNSView(context: Context) -> NSView { NSView() }

    func updateNSView(_ nsView: NSView, context: Context) {
        DispatchQueue.main.async {
            guard let window = nsView.window else { return }
            window.title = "Nucleic Settings"
            window.level = .floating
            window.styleMask.insert(.resizable)
        }
    }
}

/// Appearance: the theme, text size, and color-vision accommodation. Split into its own pane (it
/// used to sit atop the General tab) so the sidebar has a dedicated, discoverable home for how the
/// app looks — the conventional place a mature macOS app keeps these.
private struct AppearanceSettingsTab: View {
    @AppStorage(AppAppearance.storageKey) private var appearanceRaw = AppAppearance.dark.rawValue
    @AppStorage(AppTextSize.storageKey) private var textSizeRaw = AppTextSize.medium.rawValue
    @AppStorage(ColorVisionMode.storageKey) private var colorVisionRaw = ColorVisionMode.standard.rawValue
    @AppStorage(UltraGlass.enabledKey) private var ultraGlass = false

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

            Section("Materials") {
                Toggle("Ultra-glassy mode", isOn: $ultraGlass)
                Text("Backs the main pane with the sidebar's glass instead of a flat fill, "
                     + "tinted nearly opaque in the current theme color.")
                    .font(.caption).foregroundStyle(.secondary)
            }
        }
        .formStyle(.grouped)
    }
}

/// Notifications: how a chat calls you back when it finishes or needs you — completion and blocked
/// sounds, the escalating alarm, and Dock bouncing. Gathered from the old General tab into one pane
/// since they're all facets of the same "get my attention" behavior.
private struct NotificationSettingsTab: View {
    @AppStorage(ChatStatusSounds.playOnDoneKey) private var playDoneSound = true
    @AppStorage(ChatStatusSounds.playOnBlockedKey) private var playBlockedSound = true
    @AppStorage(ChatStatusSounds.escalatingAlarmKey) private var escalatingAlarm = false
    @AppStorage(DockBounce.enabledKey) private var bounceDock = true

    var body: some View {
        Form {
            Section("Sounds") {
                Toggle("Play a sound when a chat finishes", isOn: $playDoneSound)

                Toggle("Play a sound when a chat needs you", isOn: $playBlockedSound)

                Toggle("Escalating alarm until you return", isOn: $escalatingAlarm)
                    .onChange(of: escalatingAlarm) { _, isOn in
                        if !isOn { ChatAlarm.shared.setActive(false) }  // silence any alarm in progress
                    }
            }

            Section("Dock") {
                Toggle("Bounce the Dock icon until you return", isOn: $bounceDock)
                    .onChange(of: bounceDock) { _, isOn in
                        if !isOn { DockBounce.shared.setPending(false) }  // stop any bounce in progress
                    }
            }
        }
        .formStyle(.grouped)
    }
}

/// General: app-wide behavior that doesn't belong to a more specific pane — the home streak, power
/// (sleep) management, to-do labeling, diagnostics, privacy, updates, and the version footer.
/// Appearance and notification behaviors now have their own panes, leaving this one focused.
private struct GeneralSettingsTab: View {
    @AppStorage(StreakBadge.showKey) private var showStreak = true
    @AppStorage(SleepBlocker.enabledKey) private var blockSleep = false
    @AppStorage(TriageLabelStyle.encouragingKey) private var encouragingTriageLabels = true
    @AppStorage(SingularityPreparation.enabledKey) private var singularityPreparation = false
    @AppStorage(MoveDiagnostics.loggingEnabledKey) private var smartMoveLogging = false
    @AppStorage(HeartbeatSettings.shareAnonymousUsageKey) private var shareAnonymousUsage = true
    /// Auto-update controller (shared with the sidebar banner). Drives the "Updates" section's
    /// manual check; its background updates surface in the sidebar, not here.
    @EnvironmentObject private var updater: AppUpdater

    var body: some View {
        Form {
            Section("Home") {
                Toggle("Show streak counter", isOn: $showStreak)
            }

            Section("Power") {
                Toggle("Keep the Mac awake (block sleep)", isOn: $blockSleep)
                    .onChange(of: blockSleep) { _, isOn in
                        SleepBlocker.shared.setEnabled(isOn)  // start/stop the assertion immediately
                    }
                Text("Prevents your Mac from going to sleep while Nucleic is running, so long chats, "
                     + "builds, and agent work aren't interrupted. The display can still sleep.")
                    .settingsCaption()
            }

            Section("To-dos") {
                Toggle("Encouraging to-do labels", isOn: $encouragingTriageLabels)
            }

            Section("Diagnostics") {
                Toggle("Smart Move logging", isOn: $smartMoveLogging)
                Button("Reveal Log in Finder") {
                    let url = MoveDiagnostics.logFileURL
                    if FileManager.default.fileExists(atPath: url.path) {
                        NSWorkspace.shared.activateFileViewerSelecting([url])
                    } else {
                        try? FileManager.default.createDirectory(
                            at: MoveDiagnostics.supportDirectory, withIntermediateDirectories: true)
                        NSWorkspace.shared.activateFileViewerSelecting([MoveDiagnostics.supportDirectory])
                    }
                }
                .controlSize(.small)
            }

            Section("Privacy") {
                Toggle("Share anonymous usage", isOn: $shareAnonymousUsage)
            }

            Section("Singularity preparation") {
                Toggle("Prepare for the singularity", isOn: $singularityPreparation)
            }

            // Manual update check. Background updates surface as an unobtrusive sidebar banner;
            // this is the on-demand check. Shown only for builds wired for auto-update (a packaged
            // canary/beta/rc/stable embeds an appcast feed — dev/local builds keep the updater dormant).
            if updater.isConfigured {
                Section("Updates") {
                    HStack(spacing: 8) {
                        if case .found(let version) = updater.manual {
                            Button("Update Now — version \(version)") { updater.installManualUpdate() }
                                .controlSize(.small)
                        } else {
                            Button("Check for Updates…") { updater.checkForUpdates() }
                                .controlSize(.small)
                                .disabled(!updater.canCheckForUpdates)
                        }
                        updateStatusLabel
                        Spacer()
                    }
                }
            }

            // App identity footer: the running version and the build (git commit) it was
            // compiled from. Sits below the Updates section's "Check for Updates…" button and
            // stays visible even on dev/local builds, where that section is hidden — so the
            // exact build can be read (and copied for a bug report) without leaving Settings.
            HStack {
                Spacer()
                Text(Self.versionFooter)
                    .settingsCaption()
                    .textSelection(.enabled)
                Spacer()
            }
            .listRowBackground(Color.clear)
        }
        .formStyle(.grouped)
    }

    /// "Version 0.1.0 (build a1b2c3d 863)" — the marketing version (`CFBundleShortVersionString`),
    /// the commit the binary was built from, and the monotonic `CFBundleVersion` build number,
    /// all resolved once by `BuildInfo`.
    private static var versionFooter: String {
        let info = BuildInfo.current
        let build = [info.buildLabel, info.buildNumber].compactMap { $0 }.joined(separator: " ")
        return "Version \(info.version ?? "—") (build \(build))"
    }

    /// The text/spinner shown next to the update button, reflecting the last manual check.
    @ViewBuilder private var updateStatusLabel: some View {
        switch updater.manual {
        case .checking:
            ProgressView().controlSize(.small)
        case .upToDate:
            Text("You're up to date").settingsCaption()
        case .downloading(let fraction):
            Text(fraction > 0 ? "Updating… \(Int((fraction * 100).rounded()))%" : "Updating…")
                .settingsCaption()
        case .error(let message):
            Text(message).settingsCaption()
        case .idle, .found:
            EmptyView()
        }
    }
}

/// Sandbox containers: the app-wide container service and per-session sandbox containers (for
/// non-control projects). Nucleic Control settings live in their own tab.
private struct SandboxSettingsTab: View {
    @Environment(AppStore.self) private var store
    @AppStorage(ContainerServiceSettings.serviceEnabledKey) private var containerServiceEnabled = false
    @AppStorage(ContainerServiceSettings.sandboxByDefaultKey) private var sandboxByDefault = false
    @AppStorage(ContainerServiceSettings.hostExecEnabledKey) private var hostExecEnabled = true
    @AppStorage(ContainerServiceSettings.sessionContainerCPUsKey)
    private var sessionCPUs = ContainerServiceSettings.defaultContainerCPUs
    @AppStorage(ContainerServiceSettings.sessionContainerMemoryGiBKey)
    private var sessionMemoryGiB = ContainerServiceSettings.defaultContainerMemoryGiB

    /// A specific reason the in-process container runtime is unavailable on this machine, or `nil`
    /// when supported. `nil` until checked on first appearance; drives the "unsupported" hint.
    @State private var unsupportedReason: String?

    var body: some View {
        Form {
            Section("Sandbox containers") {
                // A Nucleic Control project pins the service on — it can't run without it — so the
                // toggle is forced on and locked while one exists (reconcileContainerService).
                Toggle("Enable container service", isOn: $containerServiceEnabled)
                    .disabled(store.containerServiceLockedOn)
                LearnMoreLink("How agent sandboxing works", url: SupportURL.sandboxes)

                if store.containerServiceLockedOn {
                    HStack(alignment: .firstTextBaseline, spacing: 6) {
                        Image(systemName: "lock.fill")
                            .foregroundStyle(.secondary)
                        Text("Required by your Nucleic Control projects. Remove or "
                             + "convert every Control project to turn it off.")
                            .font(.caption)
                    }
                }

                if let unsupportedReason {
                    HStack(alignment: .firstTextBaseline, spacing: 6) {
                        Image(systemName: "exclamationmark.triangle.fill")
                            .foregroundStyle(.orange)
                        Text("Container sandboxing isn't available. \(unsupportedReason)")
                            .font(.caption)
                    }
                }
            }
            .task { unsupportedReason = ContainerEngine.unsupportedReason }

            Section("New projects") {
                Toggle("Sandbox new projects by default", isOn: $sandboxByDefault)
                    .disabled(!containerServiceEnabled)
            }

            Section("Host access") {
                Toggle(isOn: $hostExecEnabled) {
                    VStack(alignment: .leading, spacing: 2) {
                        Text("Allow the host_exec tool")
                        Text("Lets sandboxed agents request to run commands on the host machine "
                             + "(outside the container) — e.g. building or running a macOS binary. "
                             + "Each call still needs your explicit approval. Turn this off to remove "
                             + "the escape hatch entirely: the tool is never offered and the agent is "
                             + "never told it exists, regardless of a project's \"Allow host build/run\" "
                             + "setting.")
                            .font(.caption).foregroundStyle(.secondary)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
                .disabled(!containerServiceEnabled)
            }

            // Per-session sandbox resource controls aren't wired up yet; show them disabled and
            // grayed out so the capability is visible but clearly not yet available.
            Section("Per-Session sandbox (coming soon)") {
                Stepper("CPUs: \(sessionCPUs)", value: $sessionCPUs, in: 1...32)
                Stepper("Memory: \(sessionMemoryGiB) GB", value: $sessionMemoryGiB, in: 1...128)
            }
            .disabled(true)
            .opacity(0.5)
        }
        .formStyle(.grouped)
    }
}

/// The Virtual Machines tab: the macOS-guest VM service (Apple `Virtualization`). Enables the
/// `mac_vm_exec` tool — each agent session gets its own isolated macOS VM for Xcode/simulator work,
/// the per-agent alternative to the shared-host `host_exec` (see docs/MACOS_VM.md). Also drives the
/// one-time golden base-image build and shows the live VMs.
private struct MacVMSettingsTab: View {
    @Environment(AppStore.self) private var store
    @AppStorage(MacVMSettings.serviceEnabledKey) private var serviceEnabled = false
    @AppStorage(MacVMSettings.exposeByDefaultKey) private var exposeByDefault = true
    @AppStorage(MacVMSettings.computerUseByDefaultKey) private var computerUseByDefault = true
    @AppStorage(MacVMSettings.axAgentEnabledKey) private var axAgentEnabled = false
    @AppStorage(MacVMSettings.autoOpenVMMonitorsKey) private var autoOpenVMMonitors = false
    @AppStorage(MacVMSettings.pictureInPictureVMMonitorsKey) private var pipVMMonitors = true
    @AppStorage(MacVMSettings.restoreImageURLKey) private var restoreImageURL = ""
    @AppStorage(MacVMSettings.restoreImageChoiceKey) private var restoreImageChoice = "macos27-devbeta"
    @AppStorage(MacVMSettings.vmCPUsKey) private var vmCPUs = MacVMSettings.defaultVMCPUs
    @AppStorage(MacVMSettings.vmMemoryGiBKey) private var vmMemoryGiB = MacVMSettings.defaultVMMemoryGiB
    @AppStorage(MacVMSettings.maxConcurrentVMsKey)
    private var maxConcurrent = MacVMSettings.defaultMaxConcurrentVMs

    // Linux VM service (a separate, independently-gated guest OS on the same Virtualization stack).
    @AppStorage(MacVMSettings.linuxServiceEnabledKey) private var linuxServiceEnabled = false
    @AppStorage(MacVMSettings.linuxExposeByDefaultKey) private var linuxExposeByDefault = true
    @AppStorage(MacVMSettings.linuxComputerUseByDefaultKey) private var linuxComputerUseByDefault = true
    @AppStorage(MacVMSettings.linuxAxAgentEnabledKey) private var linuxAxAgentEnabled = false

    // Cross-guest computer-use default + the agent-container tool gate (a container-service setting,
    // surfaced here alongside the VM controls so all virtualization knobs live together).
    @AppStorage(MacVMSettings.defaultComputerUseVMTypeKey)
    private var defaultComputerUseVMType = ComputerUseVMType.linux.rawValue
    @AppStorage(ContainerServiceSettings.agentContainersEnabledKey)
    private var agentContainersEnabled = true
    @AppStorage(ContainerServiceSettings.serviceEnabledKey) private var containerServiceEnabled = false

    @State private var baseProgress: MacVMBaseProgress?
    /// Ordered list of build phases observed so far in the current build, so completed steps stay
    /// on screen as a checklist and the whole thing visibly advances (rather than one lone spinner
    /// that reads as "frozen" during a long indeterminate phase). Reset whenever no build is running.
    @State private var seenBuildPhases: [MacVMBaseProgress.Phase] = []
    @State private var runningVMs: [MacVMEntry] = []
    @State private var baseOSVersion: String?
    @State private var baseStatus: MacVMBaseStatus?
    @State private var buildError: String?
    @State private var building = false
    @State private var deleting = false
    // Linux base build/state (the build-progress bar `baseProgress` is shared — only one base builds
    // at a time — and routed to whichever section started it via `linuxBuilding`).
    @State private var linuxBaseStatus: MacVMBaseStatus?
    @State private var linuxBuildError: String?
    @State private var linuxBuilding = false
    @State private var linuxDeleting = false
    @State private var confirmingLinuxDelete = false
    /// Drives the "Delete base image" destructive confirmation dialog.
    @State private var confirmingBaseDelete = false
    /// Whether the base-build diagnostic monitor window is currently shown.
    @State private var observingBaseVM = false
    /// Whether the (design-only) Linux "Packages…" panel is presented.
    @State private var showingPackages = false
    /// Host paths of the user-picked `.app` bundles to bake into the base image's `/Applications`
    /// (Settings → "Included apps"). Loaded from / written back to `MacVMSettings.bundledAppPaths`.
    @State private var bundledApps: [String] = []
    /// Whether an app bundle is currently being dragged over the "Included apps" drop zone.
    @State private var appDropTargeted = false
    /// True while a "Copy to running VMs" push is in flight (disables the button).
    @State private var pushingApps = false
    /// The outcome of the last "Copy to running VMs" push, shown beneath the button.
    @State private var appPushStatus: String?
    /// True while an "Add to base image" injection (boot base → copy → power off) is in flight.
    @State private var addingToBase = false
    /// The outcome of the last "Add to base image" injection, shown beneath the button.
    @State private var baseAddStatus: String?
    /// Ids of the common packages (``MacVMPackage``) opted into installing in the base image (Settings →
    /// "Common packages"). Loaded from / written back to `MacVMSettings.selectedPackageIDs`.
    @State private var selectedPackageIDs: Set<String> = []
    /// True while a "Copy to running VMs" package push is in flight (disables the button).
    @State private var pushingPackages = false
    /// The outcome of the last package "Copy to running VMs" push, shown beneath the button.
    @State private var packagePushStatus: String?
    /// True while an "Add to base image" package injection is in flight.
    @State private var addingPackagesToBase = false
    /// The outcome of the last package "Add to base image" injection, shown beneath the button.
    @State private var packageAddStatus: String?

    private var supported: Bool { store.macVMSupported }

    /// True while at least one of the two guest services is enabled — the gate for the shared
    /// session-default and resource controls, which are meaningless with no VM to run.
    private var anyServiceEnabled: Bool { serviceEnabled || linuxServiceEnabled }

    /// Single pane covering both guest OSes (macOS, Linux) and the knobs shared by both. The top of
    /// the pane holds the cross-guest switches — which engines are on, how the agent may drive them,
    /// and session defaults — so the per-platform sections below can focus purely on base images and
    /// the apps/packages baked into them.
    var body: some View {
        Form {
            virtualizationEngineSection
            computerUseSection
            macOSSection
            linuxSection
            resourcesSection
            runningNowSection
        }
        .formStyle(.grouped)
        .task { await poll() }
        .onAppear {
            reconcileBaseImageChoice()
            bundledApps = MacVMSettings.bundledAppPaths
            selectedPackageIDs = Set(MacVMSettings.selectedPackageIDs)
            // Exposing a session's own VMs is now always-on — normalize any stored opt-out.
            exposeByDefault = true
            linuxExposeByDefault = true
        }
        .sheet(isPresented: $showingPackages) { LinuxPackagesSheet() }
    }

    // MARK: - Virtualization engine (which guests are available)

    @ViewBuilder private var virtualizationEngineSection: some View {
        Section("Virtualization engine") {
            Toggle("macOS", isOn: $serviceEnabled)
                .disabled(!supported)
            Toggle("Linux", isOn: $linuxServiceEnabled)
                .disabled(!supported)
            if !supported {
                HStack(alignment: .firstTextBaseline, spacing: 6) {
                    Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange)
                    Text("Virtual machines require an Apple silicon Mac.").font(.caption)
                }
            } else {
                LearnMoreLink(url: SupportURL.virtualMachines)
            }
        }
    }

    // MARK: - Shared session defaults

    /// True when both guests are set to allow kernel-level use — the "on" state of the merged
    /// "Kernel-level use" toggle, and the gate for whether "Semantic understanding" is shown.
    private var kernelUseBoth: Bool { computerUseByDefault && linuxComputerUseByDefault }

    /// Binding for the merged "Kernel-level use" toggle. Writes both guests together; turning it off
    /// also clears both guests' semantic flag, which builds on kernel-level use.
    private var kernelUseBothBinding: Binding<Bool> {
        Binding(
            get: { kernelUseBoth },
            set: {
                computerUseByDefault = $0
                linuxComputerUseByDefault = $0
                if !$0 { axAgentEnabled = false; linuxAxAgentEnabled = false }
            })
    }

    /// Binding for the merged "Semantic understanding" toggle (both guests written together).
    private var semanticBothBinding: Binding<Bool> {
        Binding(
            get: { axAgentEnabled && linuxAxAgentEnabled },
            set: { axAgentEnabled = $0; linuxAxAgentEnabled = $0 })
    }

    // MARK: - macOS (base image + included apps)

    @ViewBuilder private var macOSSection: some View {
        Section("macOS virtual machines") {
            if let baseOSVersion {
                LabeledContent("Base image", value: "macOS \(baseOSVersion)")
            }
            if building, baseProgress == nil, !linuxBuilding {
                // `building` flips true synchronously on click, but the engine's first progress
                // phase can be up to ~a minute out — it loads/validates the cached (~14 GB) restore
                // image, or stages the provisioning share and boots the guest, before publishing a
                // phase. Without this the panel just shows the greyed-out "…" menu and the action
                // feels dead; a spinner bridges the gap until the real phase checklist takes over.
                HStack(spacing: 8) {
                    ProgressView().controlSize(.small)
                    Text("Starting base image build…").settingsCaption()
                }
            } else if let baseProgress, !linuxBuilding {
                VStack(alignment: .leading, spacing: 8) {
                    ForEach(seenBuildPhases, id: \.self) { phase in
                        buildStageRow(
                            phase,
                            active: phase == baseProgress.phase && phase != .ready,
                            fraction: baseProgress.fraction,
                            detail: phase == baseProgress.phase ? baseProgress.detail : nil)
                    }
                }
                Text("This can take a while — installing and provisioning macOS downloads several "
                    + "gigabytes and reboots the guest a few times. You can leave this and keep working; "
                    + "it runs in the background.")
                    .settingsCaption()
                Button {
                    Task {
                        observingBaseVM.toggle()
                        await store.setMacVMBaseObserver(visible: observingBaseVM)
                    }
                } label: {
                    Label(
                        observingBaseVM ? "Hide VM screen" : "Show VM screen",
                        systemImage: observingBaseVM ? "eye.slash" : "eye")
                }
            } else if let baseStatus, baseStatus.installed {
                // Built: the status line reads as "Ready — …", with the destructive/rebuild actions
                // tucked into a trailing "…" menu so the settled state stays uncluttered.
                HStack {
                    baseStatusView(baseStatus)
                    Spacer()
                    Menu {
                        Button {
                            Task { await runBaseBuild() }
                        } label: {
                            Label("Rebuild / re-provision base image", systemImage: "arrow.clockwise")
                        }
                        Button(role: .destructive) {
                            confirmingBaseDelete = true
                        } label: {
                            Label("Delete base image…", systemImage: "trash")
                        }
                    } label: {
                        Image(systemName: "ellipsis")
                    }
                    .menuStyle(.borderlessButton)
                    .menuIndicator(.hidden)
                    .fixedSize()
                    .disabled(!supported || building || deleting)
                }
            } else {
                Button {
                    Task { await runBaseBuild() }
                } label: {
                    Label("Build base image", systemImage: "square.and.arrow.down.on.square")
                        .padding(.vertical, 4)
                }
                .disabled(!supported || building || deleting)
                LearnMoreLink("How base-image builds work", url: SupportURL.macVMBaseImage)
            }
            if let buildError {
                Text(buildError).font(.caption).foregroundStyle(.red)
            }
            Picker("Base macOS version", selection: $restoreImageChoice) {
                ForEach(Self.ipswCatalog) { choice in
                    Text(choice.label).tag(choice.id)
                }
            }
            .onChange(of: restoreImageChoice) { _, newValue in
                applyIPSWChoice(newValue)
            }

            includedAppsControls

            commonPackagesControls

        }
        .disabled(!serviceEnabled || !supported)
        .confirmationDialog(
            "Delete the installed base image?", isPresented: $confirmingBaseDelete,
            titleVisibility: .visible
        ) {
            Button("Delete & reinstall (keep download)", role: .destructive) {
                Task { await deleteBaseImage(includingRestoreImages: false) }
            }
            Button("Delete & re-download everything", role: .destructive) {
                Task { await deleteBaseImage(includingRestoreImages: true) }
            }
            Button("Cancel", role: .cancel) {}
        } message: {
            Text("Removes the installed macOS base so the next build reinstalls from scratch — "
                + "unlike Rebuild, which only re-provisions the existing install. Keep the cached "
                + "~14 GB restore image for a faster reinstall, or re-download it too for a fully "
                + "clean slate. Running sessions and any configured prebuilt base are unaffected.")
        }
    }

    /// The "Included apps" controls — a drop zone plus the import/copy actions — merged inline into
    /// the macOS section. Dropped `.app` bundles are baked into the base image.
    @ViewBuilder private var includedAppsControls: some View {
        VStack(alignment: .leading, spacing: 10) {
            if !bundledApps.isEmpty {
                ForEach(bundledApps, id: \.self) { path in
                    includedAppRow(path)
                }
            }

            HStack {
                Button {
                    chooseApps()
                } label: {
                    Label("Add app…", systemImage: "plus")
                }
                if let baseStatus, baseStatus.installed {
                    Button {
                        Task { await addAppsToBase() }
                    } label: {
                        Label(
                            addingToBase ? "Importing…" : "Import",
                            systemImage: "square.and.arrow.down.on.square")
                    }
                    .disabled(addingToBase || building || deleting || bundledApps.isEmpty)
                }
                if !bundledApps.isEmpty {
                    Spacer()
                    Text("^[\(bundledApps.count) app](inflect: true)")
                        .font(.caption).foregroundStyle(.secondary)
                }
            }
        }
        .padding(8)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(
            RoundedRectangle(cornerRadius: 8)
                .strokeBorder(
                    appDropTargeted ? AnyShapeStyle(.tint) : AnyShapeStyle(.clear),
                    style: StrokeStyle(lineWidth: 2, dash: [5]))
        )
        .dropDestination(for: URL.self) { urls, _ in
            addApps(urls)
        } isTargeted: { appDropTargeted = $0 }

        if let baseAddStatus {
            Text(baseAddStatus).font(.caption).foregroundStyle(.secondary)
        }

        if !runningVMs.isEmpty {
            Button {
                Task { await pushAppsToRunningVMs() }
            } label: {
                HStack(spacing: 6) {
                    if pushingApps { ProgressView().controlSize(.small) }
                    Label(
                        "Copy to \(runningVMs.count) running VM\(runningVMs.count == 1 ? "" : "s")",
                        systemImage: "arrow.down.app")
                }
            }
            .disabled(pushingApps || bundledApps.isEmpty)
            if let appPushStatus {
                Text(appPushStatus).font(.caption).foregroundStyle(.secondary)
            }
        }
    }

    /// The "Common packages" controls — a checklist of curated tools the guest fetches and installs
    /// itself (e.g. the latest Chrome), plus the same import/copy actions as "Included apps". Entries
    /// the platform can't automate yet (``MacVMPackage/available`` == false) show grayed out with a
    /// note, so the UI advertises what's coming without letting it be selected.
    @ViewBuilder private var commonPackagesControls: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Common packages")
                .font(.callout.weight(.medium))
            Text("Tools the VM downloads and installs itself — no bundle to supply.")
                .font(.caption).foregroundStyle(.secondary)

            ForEach(MacVMPackage.catalog) { pkg in
                commonPackageRow(pkg)
            }

            HStack {
                if let baseStatus, baseStatus.installed {
                    Button {
                        Task { await addPackagesToBase() }
                    } label: {
                        Label(
                            addingPackagesToBase ? "Importing…" : "Import",
                            systemImage: "square.and.arrow.down.on.square")
                    }
                    .disabled(addingPackagesToBase || building || deleting || selectedPackageIDs.isEmpty)
                }
                if !selectedPackageIDs.isEmpty {
                    Spacer()
                    Text("^[\(selectedPackageIDs.count) package](inflect: true) selected")
                        .font(.caption).foregroundStyle(.secondary)
                }
            }
        }
        .padding(8)
        .frame(maxWidth: .infinity, alignment: .leading)

        if let packageAddStatus {
            Text(packageAddStatus).font(.caption).foregroundStyle(.secondary)
        }

        if !runningVMs.isEmpty {
            Button {
                Task { await pushPackagesToRunningVMs() }
            } label: {
                HStack(spacing: 6) {
                    if pushingPackages { ProgressView().controlSize(.small) }
                    Label(
                        "Install in \(runningVMs.count) running VM\(runningVMs.count == 1 ? "" : "s")",
                        systemImage: "arrow.down.app")
                }
            }
            .disabled(pushingPackages || selectedPackageIDs.isEmpty)
            if let packagePushStatus {
                Text(packagePushStatus).font(.caption).foregroundStyle(.secondary)
            }
        }
    }

    /// One row in the "Common packages" checklist: a toggle bound to the selection for installable
    /// packages, or a disabled row with the "coming soon" note for placeholders.
    @ViewBuilder private func commonPackageRow(_ pkg: MacVMPackage) -> some View {
        Toggle(isOn: Binding(
            get: { selectedPackageIDs.contains(pkg.id) },
            set: { on in
                if on { selectedPackageIDs.insert(pkg.id) } else { selectedPackageIDs.remove(pkg.id) }
                MacVMSettings.setSelectedPackageIDs(MacVMPackage.catalog.map(\.id)
                    .filter { selectedPackageIDs.contains($0) })
            })
        ) {
            VStack(alignment: .leading, spacing: 1) {
                Text(pkg.name).font(.callout)
                let subtext = pkg.available ? pkg.summary : (pkg.unavailableNote ?? pkg.summary)
                if !subtext.isEmpty {
                    Text(subtext)
                        .font(.caption2).foregroundStyle(.secondary)
                }
            }
        }
        .toggleStyle(.checkbox)
        .disabled(!pkg.isInstallable)
    }

    // MARK: - Linux guest sections

    @ViewBuilder private var linuxSection: some View {
        Section("Linux virtual machines") {
            if let s = linuxBaseStatus, s.installed {
                LabeledContent("Base image", value: linuxDistroLabel(s))
            }
            if let baseProgress, linuxBuilding {
                VStack(alignment: .leading, spacing: 8) {
                    ForEach(seenBuildPhases, id: \.self) { phase in
                        buildStageRow(
                            phase,
                            active: phase == baseProgress.phase && phase != .ready,
                            fraction: baseProgress.fraction,
                            detail: phase == baseProgress.phase ? baseProgress.detail : nil)
                    }
                }
                Text("This can take a while — downloading and assembling the Linux base, then "
                    + "installing the desktop and toolchain. It runs in the background; you can keep "
                    + "working.")
                    .settingsCaption()
            } else if let s = linuxBaseStatus, s.installed {
                HStack {
                    linuxBaseStatusView(s)
                    Spacer()
                    Menu {
                        Button {
                            Task { await runLinuxBaseBuild() }
                        } label: {
                            Label("Rebuild Linux base image", systemImage: "arrow.clockwise")
                        }
                        Button(role: .destructive) {
                            confirmingLinuxDelete = true
                        } label: {
                            Label("Delete Linux base image…", systemImage: "trash")
                        }
                    } label: {
                        Image(systemName: "ellipsis")
                    }
                    .menuStyle(.borderlessButton)
                    .menuIndicator(.hidden)
                    .fixedSize()
                    .disabled(!supported || linuxBuilding || linuxDeleting)
                }
            } else {
                Button {
                    Task { await runLinuxBaseBuild() }
                } label: {
                    Label("Build Linux base image", systemImage: "square.and.arrow.down.on.square")
                        .padding(.vertical, 4)
                }
                .disabled(!linuxServiceEnabled || !supported || linuxBuilding || linuxDeleting || building)
            }
            if let linuxBuildError {
                Text(linuxBuildError).font(.caption).foregroundStyle(.red)
            }

            Button {
                showingPackages = true
            } label: {
                Label("Packages…", systemImage: "shippingbox")
            }
        }
        .disabled(!linuxServiceEnabled || !supported)
        .confirmationDialog(
            "Delete the installed Linux base image?", isPresented: $confirmingLinuxDelete,
            titleVisibility: .visible
        ) {
            Button("Delete & rebuild", role: .destructive) {
                Task { await deleteLinuxBaseImage() }
            }
            Button("Cancel", role: .cancel) {}
        } message: {
            Text("Removes the installed Linux base so the next build rebuilds it from scratch. "
                + "Running sessions are unaffected.")
        }
    }

    /// The Linux base "Ready — …" status line (or an "incomplete, rebuild" note), mirroring
    /// ``baseStatusView`` for macOS.
    @ViewBuilder private func linuxBaseStatusView(_ status: MacVMBaseStatus) -> some View {
        if status.provisioned {
            Label("Ready — desktop, builds, and computer use.", systemImage: "checkmark.seal.fill")
                .font(.caption).foregroundStyle(.green)
        } else {
            Label("Installed — desktop/toolchain provisioning incomplete; rebuild.",
                systemImage: "checkmark.circle").font(.caption)
        }
    }

    /// A friendly distro label for the installed Linux base (e.g. "Ubuntu 26.04 LTS"), the Linux
    /// counterpart to the macOS "Installed guest macOS" version line.
    private func linuxDistroLabel(_ status: MacVMBaseStatus) -> String {
        if let v = status.osVersion, !v.isEmpty { return "Ubuntu \(v) LTS" }
        return "Ubuntu LTS"
    }

    // MARK: - Shared sections (apply across both guests)

    @ViewBuilder private var computerUseSection: some View {
            Section("Computer use") {
                // The two kinds of computer use, applied to whichever guests are enabled. Semantic
                // understanding builds on kernel-level use, so it only appears once that's on.
                Toggle("Kernel-level use", isOn: kernelUseBothBinding)
                    .disabled(!anyServiceEnabled || !supported)
                if kernelUseBoth {
                    Toggle("Semantic understanding", isOn: semanticBothBinding)
                        .disabled(!anyServiceEnabled || !supported)
                    LearnMoreLink(url: SupportURL.computerUse)
                }

                Picker("Default computer use VM type", selection: $defaultComputerUseVMType) {
                    ForEach(ComputerUseVMType.allCases, id: \.rawValue) { type in
                        Text(type.label).tag(type.rawValue)
                    }
                }
                .disabled(!supported)

                Toggle("Let agents create lightweight containers for simpler work",
                    isOn: $agentContainersEnabled)
                    .disabled(!containerServiceEnabled)

                Toggle("Automatically open VM monitors", isOn: $autoOpenVMMonitors)
                    .disabled(!anyServiceEnabled || !supported)
                Text("Shows the open chat's VM screen in the right column while it has a VM running, "
                    + "and closes it again when you switch to a chat without one.")
                    .settingsCaption()

                Toggle("Float VM monitors in Picture in Picture", isOn: $pipVMMonitors)
                    .disabled(!anyServiceEnabled || !supported)
                    .onChange(of: pipVMMonitors) { _, on in
                        if !on { VMMonitorPiPController.shared.hide() }  // retract immediately
                    }
                Text("Keeps the open chat's VM screen in a small window that floats above all other "
                    + "windows — even other apps — while it has a VM running. Look-only: it never "
                    + "captures your mouse or keyboard.")
                    .settingsCaption()
            }
    }

    /// Design-only preview of the Linux base "Packages…" panel: many ways to customize what's baked
    /// into the base image, all disabled until the backing is implemented.
    private struct LinuxPackagesSheet: View {
        @Environment(\.dismiss) private var dismiss
        @State private var aptPackages = ""
        @State private var snapPackages = ""
        @State private var pipPackages = ""
        @State private var repositories = ""
        @State private var provisioningScript = ""

        var body: some View {
            VStack(spacing: 0) {
                HStack {
                    VStack(alignment: .leading, spacing: 2) {
                        Text("Linux base packages").font(.headline)
                        Text("Customize what's baked into the Linux base image.")
                            .font(.caption).foregroundStyle(.secondary)
                    }
                    Spacer()
                    Text("Coming soon")
                        .font(.caption.weight(.medium))
                        .padding(.horizontal, 8).padding(.vertical, 3)
                        .background(Capsule().fill(.quaternary))
                }
                .padding(16)

                Divider()

                Form {
                    Section("System packages") {
                        TextField("APT packages", text: $aptPackages, prompt: Text("git, build-essential, jq"))
                        TextField("Snap packages", text: $snapPackages, prompt: Text("code, chromium"))
                    }
                    Section("Language packages") {
                        TextField("pip packages", text: $pipPackages, prompt: Text("numpy, requests"))
                    }
                    Section("Repositories") {
                        TextField("APT repositories / PPAs", text: $repositories,
                            prompt: Text("ppa:deadsnakes/ppa"))
                        Button {
                        } label: {
                            Label("Import package list…", systemImage: "square.and.arrow.down")
                        }
                    }
                    Section("Custom provisioning") {
                        TextEditor(text: $provisioningScript)
                            .frame(minHeight: 80)
                            .font(.system(.caption, design: .monospaced))
                        Text("Runs during the base build, after packages are installed.")
                            .settingsCaption()
                    }
                }
                .formStyle(.grouped)
                .disabled(true)   // design-only: nothing here is wired up yet

                Divider()

                HStack {
                    Spacer()
                    Button("Done") { dismiss() }
                        .keyboardShortcut(.defaultAction)
                }
                .padding(16)
            }
            .frame(width: 460, height: 560)
        }
    }

    @ViewBuilder private var resourcesSection: some View {
            Section("Per-session VM resources") {
                Stepper("CPUs: \(vmCPUs)", value: $vmCPUs, in: 1...32)
                Stepper("Memory: \(vmMemoryGiB) GB", value: $vmMemoryGiB, in: 2...128)
                Stepper("Max concurrent VMs: \(maxConcurrent)", value: $maxConcurrent, in: 1...8)
                Text("Applies to macOS and Linux guests alike. macOS caps how many macOS guests run at "
                    + "once (2 on recent releases); extra agents queue for a VM rather than failing.")
                    .settingsCaption()
            }
            .disabled((!serviceEnabled && !linuxServiceEnabled) || !supported)
    }

    @ViewBuilder private var runningNowSection: some View {
        if !runningVMs.isEmpty {
            Section("Running now") {
                ForEach(runningVMs, id: \.name) { vm in
                    LabeledContent(vm.name, value: vm.ipAddress ?? "booting…")
                }
            }
        } else {
            Section("Running now") {
                Text("No virtual machines are running right now.")
                    .settingsCaption()
            }
        }
    }

    // MARK: - Included apps (baked into the base image's /Applications)

    /// One row in the "Included apps" list: the app's Finder icon, its name, its full path, and a
    /// remove control.
    @ViewBuilder
    private func includedAppRow(_ path: String) -> some View {
        HStack(spacing: 10) {
            Image(nsImage: NSWorkspace.shared.icon(forFile: path))
                .resizable()
                .frame(width: 22, height: 22)
            VStack(alignment: .leading, spacing: 1) {
                Text(Self.appDisplayName(path)).font(.callout)
                Text(path)
                    .font(.caption2).foregroundStyle(.secondary)
                    .lineLimit(1).truncationMode(.middle)
            }
            Spacer()
            Button {
                removeApp(path)
            } label: {
                Image(systemName: "minus.circle.fill")
                    .foregroundStyle(.secondary)
            }
            .buttonStyle(.borderless)
            .help("Remove")
        }
    }

    /// The user-facing app name (bundle name minus the `.app` extension).
    private static func appDisplayName(_ path: String) -> String {
        ((path as NSString).lastPathComponent as NSString).deletingPathExtension
    }

    /// Add dragged/browsed URLs, keeping only existing `.app` bundles and de-duplicating against the
    /// current list. Returns whether anything was accepted (also the `.dropDestination` result).
    @discardableResult
    private func addApps(_ urls: [URL]) -> Bool {
        let fm = FileManager.default
        let apps = urls.filter {
            $0.pathExtension.lowercased() == "app" && fm.fileExists(atPath: $0.path)
        }
        guard !apps.isEmpty else { return false }
        var updated = bundledApps
        for url in apps where !updated.contains(url.path) {
            updated.append(url.path)
        }
        guard updated != bundledApps else { return false }
        bundledApps = updated
        MacVMSettings.setBundledAppPaths(bundledApps)
        return true
    }

    /// Drop an app from the include list (does not touch the source bundle on disk).
    private func removeApp(_ path: String) {
        bundledApps.removeAll { $0 == path }
        MacVMSettings.setBundledAppPaths(bundledApps)
    }

    /// Copy the configured apps into every running VM over vsock (no rebuild), surfacing the outcome.
    private func pushAppsToRunningVMs() async {
        pushingApps = true
        appPushStatus = nil
        defer { pushingApps = false }
        appPushStatus = await store.pushIncludedAppsToRunningMacVMs()
    }

    /// Inject the configured apps into the existing base image (boot → copy → power off), so new VMs
    /// include them without a full rebuild. Refreshes base status afterward.
    private func addAppsToBase() async {
        addingToBase = true
        // Live status while the (multi-minute) boot → copy → power-off runs: the base image also
        // appears in the sidebar's Virtual Machines section while this is in flight.
        baseAddStatus = "Booting the base image to install apps — this can take a few minutes. "
            + "It appears in the sidebar under Virtual Machines while it runs."
        defer { addingToBase = false }
        baseAddStatus = await store.addIncludedAppsToMacVMBase()
        baseStatus = await store.macVMBaseStatus()
    }

    /// Install the selected common packages into every running VM over vsock (no rebuild).
    private func pushPackagesToRunningVMs() async {
        pushingPackages = true
        packagePushStatus = nil
        defer { pushingPackages = false }
        packagePushStatus = await store.pushSelectedPackagesToRunningMacVMs()
    }

    /// Install the selected common packages into the existing base image (boot → install → power off),
    /// so new VMs include them without a full rebuild. Refreshes base status afterward.
    private func addPackagesToBase() async {
        addingPackagesToBase = true
        packageAddStatus = "Booting the base image to install packages — this can take a few minutes. "
            + "It appears in the sidebar under Virtual Machines while it runs."
        defer { addingPackagesToBase = false }
        packageAddStatus = await store.addSelectedPackagesToMacVMBase()
        baseStatus = await store.macVMBaseStatus()
    }

    /// Browse for one or more `.app` bundles to include, defaulting to `/Applications`.
    private func chooseApps() {
        let panel = NSOpenPanel()
        panel.canChooseFiles = true
        panel.canChooseDirectories = false
        panel.allowsMultipleSelection = true
        panel.allowedContentTypes = [.application]
        panel.treatsFilePackagesAsDirectories = false
        panel.directoryURL = URL(fileURLWithPath: "/Applications")
        panel.message = "Choose “.app” bundles to include in the macOS VM base image"
        panel.prompt = "Add"
        if panel.runModal() == .OK { addApps(panel.urls) }
    }

    // MARK: - Guest-image (.ipsw) catalog

    /// One selectable restore image in the hardcoded catalog. `url` is the remote `.ipsw`; an empty
    /// `url` is a placeholder (image not published yet) that falls back to "latest supported".
    private struct IPSWChoice: Identifiable, Hashable {
        let id: String
        let label: String
        let url: String
    }

    /// Hardcoded list of guest macOS restore images offered in the picker. Selecting one writes its
    /// `url` into the `restoreImageURL` setting (empty → clears it, i.e. latest supported).
    private static let ipswCatalog: [IPSWChoice] = [
        IPSWChoice(
            id: "macos27-devbeta",
            label: "macOS 27 Developer Beta",
            url: "https://updates.cdn-apple.com/2026SummerSeed/fullrestores/140-36017/"
                + "0F8D2136-7924-4F3E-A9AA-99722A9EFB39/UniversalMac_27.0_26A5378j_Restore.ipsw"),
        IPSWChoice(id: "macos27-publicbeta", label: "macOS 27 Public Beta", url: ""),
    ]

    /// Look up a catalog choice by its picker id.
    private static func ipswChoice(for id: String) -> IPSWChoice? {
        ipswCatalog.first { $0.id == id }
    }

    /// Apply a picker selection: point the restore-image URL at the chosen catalog image (empty for a
    /// not-yet-published placeholder, which the engine treats as "latest supported").
    private func applyIPSWChoice(_ id: String) {
        restoreImageURL = Self.ipswChoice(for: id)?.url ?? ""
    }

    /// Resolve the picker selection on appearance: an unknown/legacy value falls back to the default
    /// image and the chosen image's URL is kept in sync. Also clears any stale manual restore-source
    /// overrides left by the removed "Advanced" panel so they can't silently override the picker.
    private func reconcileBaseImageChoice() {
        UserDefaults.standard.removeObject(forKey: MacVMSettings.basePrebuiltPathKey)
        UserDefaults.standard.removeObject(forKey: MacVMSettings.restoreImagePathKey)
        if Self.ipswChoice(for: restoreImageChoice) == nil { restoreImageChoice = "macos27-devbeta" }
        applyIPSWChoice(restoreImageChoice)
    }

    /// Post-build status: exec-ready vs. fully computer-use-ready. Semantic AX ships in the base
    /// image and its grants are written automatically during the build, so once the base is
    /// provisioned everything — builds, computer use, and semantic AX — is ready.
    @ViewBuilder
    private func baseStatusView(_ status: MacVMBaseStatus) -> some View {
        // Computer use works host-side on any installed base; a fully provisioned base adds the toolchain.
        if status.provisioned {
            Label("Ready — builds + computer use + semantic AX.", systemImage: "checkmark.seal.fill")
                .font(.caption).foregroundStyle(.green)
        } else {
            Label("Computer use ready. Toolchain provisioning incomplete — rebuild.",
                systemImage: "checkmark.circle").font(.caption)
        }
    }

    private func runBaseBuild() async {
        building = true
        buildError = nil
        defer { building = false }
        do {
            try await store.buildMacVMBaseImage(localRestoreImagePath: nil)
        } catch {
            buildError = "\(error)"
        }
    }

    /// Kick off the Linux base build. `linuxBuilding` routes the shared progress bar to the Linux
    /// section (only one base builds at a time).
    private func runLinuxBaseBuild() async {
        linuxBuilding = true
        linuxBuildError = nil
        defer { linuxBuilding = false }
        do {
            try await store.buildLinuxVMBaseImage()
        } catch {
            linuxBuildError = "\(error)"
        }
        linuxBaseStatus = await store.linuxVMBaseStatus()
    }

    /// Delete the installed Linux base so the next build rebuilds from scratch.
    private func deleteLinuxBaseImage() async {
        linuxDeleting = true
        linuxBuildError = nil
        defer { linuxDeleting = false }
        do {
            try await store.deleteLinuxVMBaseImage()
            linuxBaseStatus = await store.linuxVMBaseStatus()
        } catch {
            linuxBuildError = "\(error)"
        }
    }

    /// Delete the installed base (and cached restore image) so the next build reinstalls macOS from
    /// scratch. Refreshes status immediately so the panel flips back to "Build base image".
    private func deleteBaseImage(includingRestoreImages: Bool) async {
        deleting = true
        buildError = nil
        defer { deleting = false }
        do {
            try await store.deleteMacVMBaseImage(includingRestoreImages: includingRestoreImages)
            baseStatus = await store.macVMBaseStatus()
            baseOSVersion = await store.macVMBaseOSVersion()
        } catch {
            buildError = "\(error)"
        }
    }

    /// Poll base-build progress + the live VM list while the tab is open.
    private func poll() async {
        while !Task.isCancelled {
            let progress = await store.macVMBaseProgress()
            recordBuildPhase(progress)
            baseProgress = progress
            // The build ended (surface detached, monitor auto-hidden) — reset the toggle to match.
            if progress == nil, observingBaseVM {
                observingBaseVM = false
                await store.setMacVMBaseObserver(visible: false)
            }
            runningVMs = await store.runningMacVMs()
            baseOSVersion = await store.macVMBaseOSVersion()
            baseStatus = await store.macVMBaseStatus()
            linuxBaseStatus = await store.linuxVMBaseStatus()
            try? await Task.sleep(for: .seconds(2))
        }
    }

    /// Grow (or reset) the observed-phase checklist. Phases are reported one at a time and advance
    /// monotonically, so appending each newly-seen phase preserves build order; a `nil` progress
    /// (no build running) clears the list for the next run.
    private func recordBuildPhase(_ progress: MacVMBaseProgress?) {
        guard let progress else {
            if !seenBuildPhases.isEmpty { seenBuildPhases = [] }
            return
        }
        if !seenBuildPhases.contains(progress.phase) {
            seenBuildPhases.append(progress.phase)
        }
    }

    /// One row in the build checklist: a green check for a completed stage, or the stage label plus
    /// an animated linear bar (indeterminate, or determinate when the phase reports a fraction) for
    /// the stage in progress.
    @ViewBuilder
    private func buildStageRow(
        _ phase: MacVMBaseProgress.Phase, active: Bool, fraction: Double?, detail: String? = nil
    ) -> some View {
        HStack(alignment: .top, spacing: 8) {
            Image(systemName: active ? "circle.fill" : "checkmark.circle.fill")
                .font(.caption2)
                .foregroundStyle(active ? AnyShapeStyle(.tint) : AnyShapeStyle(.green))
                .frame(width: 14)
                .padding(.top, 1)
            VStack(alignment: .leading, spacing: 4) {
                Text(MacVMBaseProgress(phase: phase).label)
                    .font(.caption)
                    .fontWeight(active ? .semibold : .regular)
                    .foregroundStyle(active ? .primary : .secondary)
                if active {
                    if let fraction {
                        ProgressView(value: fraction).progressViewStyle(.linear)
                    } else {
                        ProgressView().progressViewStyle(.linear)
                    }
                    if let detail, !detail.isEmpty {
                        Text(detail)
                            .font(.caption2)
                            .foregroundStyle(.secondary)
                            .lineLimit(2)
                            .textSelection(.enabled)
                    }
                }
            }
        }
    }
}

/// The Git tab: Nucleic's Managed Git credential and where it's used. The credential is always
/// used for Nucleic's own host-side git (cloning a project, pushing a branch, opening a PR) and is
/// optionally made available to sandboxed sessions. The SSH config section re-supplies any
/// connectivity Managed Git's hermetic ssh would otherwise miss.
private struct GitSettingsTab: View {
    var body: some View {
        Form {
            GitHubAccessSection()
            ManagedGitSSHConfigSection()
        }
        .formStyle(.grouped)
    }
}

/// Extra OpenSSH config for Managed Git's connections to GitHub. Managed Git deliberately ignores
/// the user's real `~/.ssh/config` (to stay off their personal keys and ssh-agent), which also drops
/// any connectivity directives there — so this section lets the user re-supply just the connectivity
/// they need (a custom `Hostname`/`Port`, a `ProxyCommand`…). Identity/agent directives are ignored:
/// Managed Git always uses its own key. Applies to host and sandbox ssh alike, and only in the SSH /
/// Nucleic-managed-key modes (it's inert for token/none).
private struct ManagedGitSSHConfigSection: View {
    @AppStorage(GitHubCredentialSettings.sshConfigKey) private var sshConfig = ""

    var body: some View {
        Section("SSH config") {
            TextEditor(text: $sshConfig)
                .font(.system(.caption, design: .monospaced))
                .frame(minHeight: 80)
                .overlay(RoundedRectangle(cornerRadius: 4).stroke(.quaternary))
        }
    }
}

/// The app-level Managed Git credential. It's always used for Nucleic's own host-side `git`
/// (cloning a project, pushing a branch, opening a PR) — pinned via SSH `IdentitiesOnly` so the
/// host never falls back to the user's personal `~/.ssh` keys — and is optionally injected into
/// sandboxed sessions ("Make available in sandbox"; Control sessions always get it). The secret
/// lives in the login Keychain (via `GitHubCredentialStore`), never in UserDefaults; only the mode
/// and flags are plain preferences. The Nucleic-managed key never surfaces its private half — only
/// its public key can be copied or exported.
private struct GitHubAccessSection: View {
    @Environment(AppStore.self) private var store

    @AppStorage(GitHubCredentialSettings.authModeKey) private var modeRaw = GitHubAuthMode.none.rawValue
    @AppStorage(GitHubCredentialSettings.signCommitsKey) private var signCommits = false
    @AppStorage(GitHubCredentialSettings.ghForceSSHKey) private var ghForceSSH = false
    @AppStorage(GitHubCredentialSettings.gitForceSSHKey) private var gitForceSSH = false
    @AppStorage(GitHubCredentialSettings.availableInSandboxKey) private var availableInSandbox = true

    /// Draft input for the secret being entered; never pre-filled from the Keychain (we don't
    /// read secrets back into the UI). `tokenStored` / `keyStored` reflect whether one is saved.
    @State private var tokenDraft = ""
    @State private var keyDraft = ""
    /// A filesystem path to load a key from (the file-picker fills this; it can also be typed).
    @State private var pathDraft = ""
    /// Passphrase for an encrypted key being saved. Taken verbatim (never trimmed).
    @State private var passphraseDraft = ""
    @State private var tokenStored = false
    @State private var keyStored = false
    @State private var passphraseStored = false
    /// A validation complaint about the pasted/loaded SSH key (not a real private key, or
    /// encrypted-without-a-passphrase).
    @State private var keyWarning: String?
    /// The Nucleic-managed public key, when one has been generated; drives the `.managed` UI.
    @State private var managedPublicKey: String?
    /// A failure from generating the managed key.
    @State private var managedError: String?

    private var mode: GitHubAuthMode { GitHubAuthMode(rawValue: modeRaw) ?? .none }

    /// True when Nucleic Control is in use — either turned on for new projects, or there's already
    /// at least one Control project. Control sessions run in the shared container, which has no other
    /// way to reach GitHub, so the credential is always made available to them; the toggle is then
    /// forced on and locked.
    private var controlActive: Bool {
        ContainerServiceSettings.controlByDefault
            || store.projects.contains { $0.isNucleicControlled }
    }

    var body: some View {
        Section("Managed Git") {
            Picker("Authentication", selection: $modeRaw) {
                Text("None").tag(GitHubAuthMode.none.rawValue)
                Text("Token (HTTPS)").tag(GitHubAuthMode.token.rawValue)
                Text("SSH key").tag(GitHubAuthMode.ssh.rawValue)
                Text("Nucleic-managed key").tag(GitHubAuthMode.managed.rawValue)
            }

            switch mode {
            case .none:
                EmptyView()
            case .token:
                tokenControls
            case .ssh:
                sshKeyEntry
                sshOptions
            case .managed:
                managedKeyEntry
                sshOptions
            }

            // Whether sandboxed sessions also get this credential. Control sessions always do (their
            // shared container can't reach GitHub otherwise), so it's forced on and locked then.
            if controlActive {
                Toggle("Make available in sandbox", isOn: .constant(true))
                    .disabled(true)
            } else {
                Toggle("Make available in sandbox", isOn: $availableInSandbox)
                    .disabled(mode == .none)
            }
        }
        .onAppear {
            refreshStoredFlags()
            managedPublicKey = ManagedSSHKey.publicKey
            // Keep the stored value consistent with the locked-on display, so per-session sandboxes
            // also see it as available while Control is in use.
            if controlActive { availableInSandbox = true }
        }
    }

    // MARK: Token

    @ViewBuilder private var tokenControls: some View {
        tokenEntry()
    }

    /// Save/clear UI for the stored PAT. Backed by the one Keychain token slot, so the same secret
    /// serves token mode (git + gh) and SSH mode (gh API).
    @ViewBuilder private func tokenEntry() -> some View {
        if tokenStored {
            storedRow(label: "Token saved") {
                GitHubCredentialStore.deleteToken()
                refreshStoredFlags()
            }
        } else {
            SecureField("Personal access token", text: $tokenDraft)
            Button("Save token") {
                GitHubCredentialStore.saveToken(tokenDraft.trimmingCharacters(in: .whitespacesAndNewlines))
                tokenDraft = ""
                refreshStoredFlags()
            }
            .disabled(tokenDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
        }
    }

    // MARK: SSH (user-supplied key)

    @ViewBuilder private var sshKeyEntry: some View {
        if keyStored {
            storedRow(label: passphraseStored ? "SSH key saved (with passphrase)" : "SSH key saved") {
                GitHubCredentialStore.deleteSSHPrivateKey()
                GitHubCredentialStore.deleteSSHPassphrase()
                refreshStoredFlags()
            }
        } else {
            TextEditor(text: $keyDraft)
                .font(.system(.caption, design: .monospaced))
                .frame(minHeight: 90)
                .overlay(RoundedRectangle(cornerRadius: 4).stroke(.quaternary))
            HStack {
                Button("Choose File…") { chooseKeyFile() }
                TextField("or type a key path", text: $pathDraft)
                    .textFieldStyle(.roundedBorder)
                Button("Load") { loadKey(from: pathDraft) }
                    .disabled(pathDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
            }
            SecureField("Passphrase (only if the key is encrypted)", text: $passphraseDraft)
            if let keyWarning {
                Label(keyWarning, systemImage: "exclamationmark.triangle.fill")
                    .font(.caption)
                    .foregroundStyle(.orange)
            }
            Button("Save SSH key") { saveSSHKey() }
                .disabled(keyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
        }
    }

    private func saveSSHKey() {
        let key = keyDraft.trimmingCharacters(in: .whitespacesAndNewlines)
        guard key.contains("PRIVATE KEY") else {
            keyWarning = "That doesn't look like an OpenSSH private key (expected a "
                + "“-----BEGIN … PRIVATE KEY-----” block)."
            return
        }
        // Passphrases can contain meaningful whitespace, so take the field verbatim.
        let passphrase = passphraseDraft
        if key.contains("ENCRYPTED") && passphrase.isEmpty {
            keyWarning = "This key is passphrase-protected. Enter its passphrase below so the "
                + "sandbox can unlock it non-interactively."
            return
        }
        keyWarning = nil
        GitHubCredentialStore.saveSSHPrivateKey(key)
        if passphrase.isEmpty {
            GitHubCredentialStore.deleteSSHPassphrase()
        } else {
            GitHubCredentialStore.saveSSHPassphrase(passphrase)
        }
        keyDraft = ""
        passphraseDraft = ""
        pathDraft = ""
        refreshStoredFlags()
    }

    /// Open a file picker for a private key and load its contents into the editor. `~/.ssh` is
    /// hidden, so show hidden files.
    private func chooseKeyFile() {
        let panel = NSOpenPanel()
        panel.canChooseFiles = true
        panel.canChooseDirectories = false
        panel.allowsMultipleSelection = false
        panel.showsHiddenFiles = true
        panel.message = "Choose an OpenSSH private key"
        if panel.runModal() == .OK, let url = panel.url { loadKey(from: url.path) }
    }

    /// Read a private key from `path` (tilde-expanded) into the editor, where the usual Save flow
    /// validates it. We don't save straight from disk so the user can confirm/add a passphrase first.
    private func loadKey(from path: String) {
        let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }
        let expanded = (trimmed as NSString).expandingTildeInPath
        do {
            keyDraft = try String(contentsOfFile: expanded, encoding: .utf8)
            pathDraft = ""
            keyWarning = nil
        } catch {
            keyWarning = "Couldn't read a key from \(expanded): \(error.localizedDescription)"
        }
    }

    // MARK: Nucleic-managed key

    @ViewBuilder private var managedKeyEntry: some View {
        if let pub = managedPublicKey {
            Label("Key generated", systemImage: "checkmark.seal.fill")
                .foregroundStyle(.green)
            Text(pub)
                .font(.system(.caption, design: .monospaced))
                .textSelection(.enabled)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(6)
                .background(RoundedRectangle(cornerRadius: 4).fill(.quaternary.opacity(0.4)))
            HStack {
                Button("Copy public key") { copyToPasteboard(pub) }
                Button("Export…") { exportPublicKey(pub) }
                Spacer()
                Button("Regenerate", role: .destructive) { generateManagedKey() }
                Button("Remove", role: .destructive) {
                    ManagedSSHKey.delete()
                    managedPublicKey = nil
                }
            }
        } else {
            Button("Generate Nucleic-managed key") { generateManagedKey() }
            if let managedError {
                Label(managedError, systemImage: "exclamationmark.triangle.fill")
                    .font(.caption)
                    .foregroundStyle(.orange)
            }
        }
    }

    private func generateManagedKey() {
        do {
            managedPublicKey = try ManagedSSHKey.generate()
            managedError = nil
        } catch {
            managedError = "Couldn't generate a key: \(error.localizedDescription)"
        }
    }

    private func exportPublicKey(_ pub: String) {
        let panel = NSSavePanel()
        panel.nameFieldStringValue = "nucleic-managed.pub"
        panel.canCreateDirectories = true
        if panel.runModal() == .OK, let url = panel.url {
            try? (pub + "\n").write(to: url, atomically: true, encoding: .utf8)
        }
    }

    private func copyToPasteboard(_ value: String) {
        NSPasteboard.general.clearContents()
        NSPasteboard.general.setString(value, forType: .string)
    }

    // MARK: Shared SSH options (apply to both .ssh and .managed)

    @ViewBuilder private var sshOptions: some View {
        Toggle("Sign commits with this SSH key", isOn: $signCommits)

        Toggle("Force gh to use SSH", isOn: $ghForceSSH)

        Toggle("Restrict git to SSH for GitHub", isOn: $gitForceSSH)

        Text("API token for gh (optional)")
            .font(.callout.weight(.medium))
        tokenEntry()
    }

    // MARK: Helpers

    @ViewBuilder private func storedRow(label: String, clear: @escaping () -> Void) -> some View {
        HStack {
            Label(label, systemImage: "checkmark.seal.fill")
                .foregroundStyle(.green)
            Spacer()
            Button("Clear", role: .destructive, action: clear)
        }
    }

    private func refreshStoredFlags() {
        tokenStored = !(GitHubCredentialStore.loadToken() ?? "").isEmpty
        keyStored = !(GitHubCredentialStore.loadSSHPrivateKey() ?? "").isEmpty
        passphraseStored = !(GitHubCredentialStore.loadSSHPassphrase() ?? "").isEmpty
    }
}

/// Nucleic Control: repos Nucleic clones under `~/.nucleic/control/` and manages end-to-end
/// (shared sandbox container, the `git` interceptor, autoship-eligible). Requires the container
/// service (Sandbox tab).
private struct ControlSettingsTab: View {
    @Environment(AppStore.self) private var store

    @AppStorage(ContainerServiceSettings.serviceEnabledKey) private var containerServiceEnabled = false
    @AppStorage(ContainerServiceSettings.controlByDefaultKey) private var controlByDefault = false
    @AppStorage(ContainerServiceSettings.nvrsionByDefaultKey) private var nvrsionByDefault = false
    @AppStorage(ContainerServiceSettings.controlContainerCPUsKey)
    private var controlCPUs = ContainerServiceSettings.defaultControlContainerCPUs
    @AppStorage(ContainerServiceSettings.controlContainerMemoryGiBKey)
    private var controlMemoryGiB = ContainerServiceSettings.recommendedControlContainerMemoryGiB()
    @AppStorage(ContainerServiceSettings.controlAuthModeKey)
    private var authMode = ControlAuthMode.oauth.rawValue
    @AppStorage(ContainerServiceSettings.codexControlAuthModeKey)
    private var codexAuthMode = ControlAuthMode.oauth.rawValue
    @AppStorage(ContainerServiceSettings.splitControlContainersByBackendKey)
    private var splitContainersByBackend = false
    @AppStorage(ContainerServiceSettings.memoryManagementKey)
    private var memoryManagement = MemoryManagementLevel.conservative.rawValue
    @AppStorage(ContainerServiceSettings.commandTracingEnabledKey)
    private var commandTracing = false
    @AppStorage(ContainerServiceSettings.autoCheckImageUpdatesKey)
    private var autoCheckImageUpdates = true
    /// Draft for the API-key entry field (write-only — the stored key is never read back into UI).
    @State private var apiKeyDraft = ""
    /// Whether a key is currently in the Keychain, for the "Key saved" / Clear affordances.
    @State private var keySaved = ControlAPIKeyStore.hasKey
    /// Codex API-key draft + saved-state, siblings of the Claude ones above.
    @State private var codexAPIKeyDraft = ""
    @State private var codexKeySaved = CodexControlAPIKeyStore.hasKey

    /// True while a force-recreation is in flight, to disable the button and show progress.
    @State private var recreating = false
    /// True while a memory-reclaiming restart is in flight.
    @State private var restarting = false
    /// True while an on-demand "Reclaim memory now" pass is in flight.
    @State private var reclaiming = false
    /// True while an in-place "Check for updates" (agent-CLI refresh) is in flight.
    @State private var checkingUpdates = false
    /// Result of the last "Check for updates" run, presented in a confirmation alert. `nil` hides it.
    @State private var updateResults: [AgentCLIUpdateResult]?

    /// Upper bound for the memory stepper: this Mac's total physical RAM. `--memory` is a ceiling
    /// the VM grows into, so allocating more than the host has would be meaningless.
    private var maxControlMemoryGiB: Int {
        max(1, Int(ProcessInfo.processInfo.physicalMemory / 1_073_741_824))
    }

    var body: some View {
        Form {
            Section("Nucleic Control") {
                if !containerServiceEnabled {
                    HStack(alignment: .firstTextBaseline, spacing: 6) {
                        Image(systemName: "exclamationmark.triangle.fill")
                            .foregroundStyle(.orange)
                        Text("Turn on the container service in Settings → Sandbox to use Nucleic "
                             + "Control.")
                            .font(.caption)
                    }
                }

                Toggle("Enable Nucleic Control on all projects", isOn: $controlByDefault)
                    .disabled(!containerServiceEnabled)
                LearnMoreLink("What is Nucleic Control?", url: SupportURL.nucleicControl)
            }

            Section {
                if !containerServiceEnabled {
                    HStack(alignment: .firstTextBaseline, spacing: 6) {
                        Image(systemName: "exclamationmark.triangle.fill")
                            .foregroundStyle(.orange)
                        Text("Turn on the container service in Settings → Sandbox to use nvrsion.")
                            .font(.caption)
                    }
                }

                Toggle("Use nvrsion for new Control projects", isOn: $nvrsionByDefault)
                    .disabled(!containerServiceEnabled)
            } header: {
                HStack(spacing: 6) {
                    Text("nvrsion")
                    Text("BETA")
                        .font(.caption2).bold()
                        .padding(.horizontal, 5).padding(.vertical, 1)
                        .background(AppTheme.hairline, in: .capsule)
                }
            }

            Section("Container authentication") {
                Picker("Authenticate Claude with", selection: $authMode) {
                    Text("Claude subscription (OAuth)").tag(ControlAuthMode.oauth.rawValue)
                    Text("Anthropic API key").tag(ControlAuthMode.apiKey.rawValue)
                }
                .disabled(!containerServiceEnabled)

                if authMode == ControlAuthMode.apiKey.rawValue {
                    SecureField("sk-ant-…", text: $apiKeyDraft)
                        .disabled(!containerServiceEnabled)
                    HStack(spacing: 10) {
                        Button("Save Key") {
                            if ControlAPIKeyStore.save(apiKeyDraft) { keySaved = ControlAPIKeyStore.hasKey }
                            apiKeyDraft = ""
                        }
                        .disabled(!containerServiceEnabled
                                  || apiKeyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
                        if keySaved {
                            Button("Clear", role: .destructive) {
                                ControlAPIKeyStore.clear()
                                keySaved = false
                            }
                            Label("Key saved", systemImage: "checkmark.seal.fill")
                                .foregroundStyle(.green).font(.caption)
                        }
                    }
                }

                Picker("Authenticate Codex with", selection: $codexAuthMode) {
                    Text("ChatGPT subscription (OAuth)").tag(ControlAuthMode.oauth.rawValue)
                    Text("OpenAI API key").tag(ControlAuthMode.apiKey.rawValue)
                }
                .disabled(!containerServiceEnabled)

                if codexAuthMode == ControlAuthMode.apiKey.rawValue {
                    SecureField("sk-…", text: $codexAPIKeyDraft)
                        .disabled(!containerServiceEnabled)
                    HStack(spacing: 10) {
                        Button("Save Key") {
                            if CodexControlAPIKeyStore.save(codexAPIKeyDraft) { codexKeySaved = CodexControlAPIKeyStore.hasKey }
                            codexAPIKeyDraft = ""
                        }
                        .disabled(!containerServiceEnabled
                                  || codexAPIKeyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
                        if codexKeySaved {
                            Button("Clear", role: .destructive) {
                                CodexControlAPIKeyStore.clear()
                                codexKeySaved = false
                            }
                            Label("Key saved", systemImage: "checkmark.seal.fill")
                                .foregroundStyle(.green).font(.caption)
                        }
                    }
                } else {
                    Text("Reused from your host `~/.codex` login and kept in sync after each turn, so host and container no longer revoke each other's refresh token.")
                        .font(.caption).foregroundStyle(.secondary)
                }
            }

            Section("Nucleic Control container") {
                LabeledContent("CPUs", value: "Auto")
                LabeledContent("Memory", value: "Auto")

                Picker("Memory management", selection: $memoryManagement) {
                    ForEach(MemoryManagementLevel.allCases, id: \.rawValue) { level in
                        Text(level.label).tag(level.rawValue)
                    }
                }
                .disabled(!containerServiceEnabled)

                Toggle("Agent Lifecycle Protection", isOn: $splitContainersByBackend)
                    .disabled(!containerServiceEnabled)

                Toggle("Trace shell commands", isOn: $commandTracing)
                    .disabled(!containerServiceEnabled)

                Toggle("Automatically check for image updates", isOn: $autoCheckImageUpdates)
                    .disabled(!containerServiceEnabled)
                    .onChange(of: autoCheckImageUpdates) { store.startImageUpdateChecksIfNeeded() }
                    .help("Periodically check the registry for a newer sandbox base image (a small "
                        + "digest check, no download) and surface it in Maintenance so you can apply "
                        + "it without waiting for an app update.")
            }

            Section("Maintenance") {
                Button {
                    checkingUpdates = true
                    Task {
                        // Check the registry for a newer base image (cheap digest HEAD) and refresh
                        // the in-place CLIs together — one "check for updates" covers both assets.
                        async let image = store.checkForImageUpdate(userInitiated: true)
                        async let clis = store.updateControlAgentCLIs()
                        let (_, results) = await (image, clis)
                        checkingUpdates = false
                        updateResults = results
                    }
                } label: {
                    HStack(spacing: 6) {
                        if checkingUpdates { ProgressView().controlSize(.small) }
                        Text(checkingUpdates ? "Checking…" : "Check for updates")
                    }
                }
                .disabled(!containerServiceEnabled || checkingUpdates)
                .help("Check the registry for a newer sandbox base image and update the Codex and "
                    + "Claude Code CLIs in the running container to their latest versions — instant "
                    + "access to new models. Applying a base-image update uses Force re-creation.")

                imageUpdateStatusRow

                Button {
                    reclaiming = true
                    Task {
                        await store.reclaimControlMemory()
                        reclaiming = false
                    }
                } label: {
                    HStack(spacing: 6) {
                        if reclaiming { ProgressView().controlSize(.small) }
                        Text(reclaiming ? "Reclaiming…" : "Reclaim memory now")
                    }
                }
                .disabled(!containerServiceEnabled || reclaiming)

                Button {
                    restarting = true
                    Task {
                        await store.restartControlSandbox()
                        restarting = false
                    }
                } label: {
                    HStack(spacing: 6) {
                        if restarting { ProgressView().controlSize(.small) }
                        Text(restarting ? "Restarting…" : "Restart container")
                    }
                }
                .disabled(!containerServiceEnabled || restarting || recreating)

                Button(role: .destructive) {
                    recreating = true
                    Task {
                        await store.recreateControlSandbox()
                        recreating = false
                    }
                } label: {
                    HStack(spacing: 6) {
                        if recreating { ProgressView().controlSize(.small) }
                        Text(recreating ? "Re-creating…" : "Force re-creation")
                    }
                }
                .disabled(!containerServiceEnabled || recreating || restarting)
            }
        }
        .formStyle(.grouped)
        .task {
            // Refresh the base-image update status when the tab opens (cheap digest check; no-op when
            // the service is off), so the Maintenance surface reflects the registry without waiting
            // for the periodic background loop.
            await store.checkForImageUpdate()
        }
        .alert("Check for updates", isPresented: Binding(
            get: { updateResults != nil },
            set: { if !$0 { updateResults = nil } }
        ), presenting: updateResults) { _ in
            Button("OK", role: .cancel) { updateResults = nil }
        } message: { results in
            Text(Self.imageStatusLine(store.imageUpdateStatus) + Self.updateSummary(results))
        }
    }

    /// One-line base-image verdict prepended to the "Check for updates" alert.
    private static func imageStatusLine(_ status: ImageUpdateStatus) -> String {
        switch status {
        case .updateAvailable:
            return "Base image: a newer version is available — apply it with Force re-creation.\n\n"
        case .upToDate:
            return "Base image: up to date.\n\n"
        case .notInstalled:
            return "Base image: not downloaded yet.\n\n"
        case .unknown:
            return "Base image: couldn't check (offline or registry unreachable).\n\n"
        case .idle, .checking:
            return ""
        }
    }

    /// A row reflecting the sandbox base-image asset auto-update check (`store.imageUpdateStatus`).
    /// Invites the user to apply an available update via Force re-creation; otherwise a subtle status
    /// line (or nothing before the first check / when the registry couldn't be reached).
    @ViewBuilder private var imageUpdateStatusRow: some View {
        switch store.imageUpdateStatus {
        case .updateAvailable:
            HStack(spacing: 8) {
                Label("A newer sandbox image is available", systemImage: "shippingbox")
                    .foregroundStyle(.orange)
                Spacer()
                Button("Update now") {
                    recreating = true
                    Task {
                        await store.applyImageUpdate()
                        recreating = false
                    }
                }
                .disabled(recreating || restarting)
            }
        case .checking:
            Label("Checking for image updates…", systemImage: "arrow.triangle.2.circlepath")
                .foregroundStyle(.secondary).font(.caption)
        case .upToDate:
            Label("Sandbox image is up to date", systemImage: "checkmark.circle")
                .foregroundStyle(.secondary).font(.caption)
        case .notInstalled:
            Label("Sandbox image not downloaded yet", systemImage: "arrow.down.circle")
                .foregroundStyle(.secondary).font(.caption)
        case .idle, .unknown:
            EmptyView()
        }
    }

    /// Human-readable summary of a "Check for updates" run for the confirmation alert.
    private static func updateSummary(_ results: [AgentCLIUpdateResult]) -> String {
        guard !results.isEmpty else {
            return "No running control container to update. Start a control chat (or use Force "
                + "re-creation to rebuild the base image), then try again."
        }
        return results.map { r in
            let head = r.success ? "✓ \(r.container)" : "✗ \(r.container)"
            let detail = r.detail.isEmpty ? "" : "\n\(r.detail)"
            return head + detail
        }.joined(separator: "\n\n")
    }
}

/// Everything about the chat experience: how the transcript renders, the composer
/// send key, and how completed chats are archived. The auto-archive policy syncs
/// to `AppStore` via `pushDefaults()`.
private struct ChatSettingsTab: View {
    @Environment(AppStore.self) private var store

    @AppStorage(TranscriptDisplay.showDebugKey) private var showDebugLines = false
    @AppStorage(TranscriptDisplay.showLockEventsKey) private var showLockEvents = true
    @AppStorage(SubmitKeyMode.storageKey) private var submitKeyRaw = SubmitKeyMode.modifierSends.rawValue
    @AppStorage(AutoArchivePolicy.storageKey) private var autoArchiveRaw = AutoArchivePolicy.fallback.rawValue
    @AppStorage(ArchivedWorktreeCleanupPolicy.storageKey) private var worktreeCleanupRaw = ArchivedWorktreeCleanupPolicy.fallback.rawValue
    @AppStorage(EphemeralChatPolicy.savedByDefaultKey) private var scratchSavedByDefault = true

    private var submitMode: SubmitKeyMode { SubmitKeyMode(rawValue: submitKeyRaw) ?? .modifierSends }
    private var autoArchivePolicy: AutoArchivePolicy { AutoArchivePolicy(rawValue: autoArchiveRaw) ?? .fallback }
    private var worktreeCleanupPolicy: ArchivedWorktreeCleanupPolicy {
        ArchivedWorktreeCleanupPolicy(rawValue: worktreeCleanupRaw) ?? .fallback
    }

    var body: some View {
        Form {
            Section("Transcript") {
                Toggle("Show advanced detail", isOn: $showDebugLines)

                Toggle("Show lock events", isOn: $showLockEvents)
            }

            Section("Composer") {
                Picker("Send with", selection: $submitKeyRaw) {
                    ForEach(SubmitKeyMode.allCases) { Text($0.label).tag($0.rawValue) }
                }
            }

            Section("Archiving") {
                Picker("Auto-archive completed chats", selection: $autoArchiveRaw) {
                    ForEach(AutoArchivePolicy.allCases) { Text($0.label).tag($0.rawValue) }
                }

                Picker("Delete archived chat worktrees", selection: $worktreeCleanupRaw) {
                    ForEach(ArchivedWorktreeCleanupPolicy.allCases) { Text($0.label).tag($0.rawValue) }
                }
            }

            Section("Scratch chats") {
                Toggle("Keep scratch chats by default", isOn: $scratchSavedByDefault)
                Text(scratchSavedByDefault
                    ? "New out-of-project (Scratch) chats are kept when you close their window; switch a chat to Ephemeral in its window to discard it on close."
                    : "New out-of-project (Scratch) chats are discarded — along with their scratch workspace — when you close their window; switch a chat to Saved in its window to keep it.")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        }
        .formStyle(.grouped)
        .onAppear { pushDefaults() }
        .onChange(of: autoArchiveRaw) { _, _ in pushDefaults() }
        .onChange(of: worktreeCleanupRaw) { _, _ in pushDefaults() }
    }

    private func pushDefaults() {
        store.autoArchiveIdleInterval = autoArchivePolicy.interval
        store.archivedWorktreeCleanupInterval = worktreeCleanupPolicy.interval
    }
}

/// Defaults applied to new chats (model/effort/auto/autoship), synced to `AppStore` via
/// `pushDefaults()`. The default model also picks the backend (`BackendID.forModel`): a Claude
/// SKU runs on Claude Code, a GPT SKU on Codex.
private struct AgentsSettingsTab: View {
    @Environment(AppStore.self) private var store

    @AppStorage(ModelCatalog.defaultModelKey) private var defaultModel = ModelCatalog.fallbackModel
    @AppStorage(ModelCatalog.defaultEffortKey) private var defaultEffort = ModelCatalog.fallbackEffort
    @AppStorage(ModelCatalog.defaultSupervisorModelKey) private var defaultSupervisorModel = ModelCatalog.fallbackSupervisorModel
    @AppStorage(ModelCatalog.defaultOrchestraWorkerModelKey) private var defaultOrchestraWorkerModel = ModelCatalog.fallbackOrchestraWorkerModel
    @AppStorage(ModelCatalog.defaultOrchestraMaxWorkersKey) private var defaultOrchestraMaxWorkers = ModelCatalog.fallbackOrchestraMaxWorkers
    @AppStorage(ModelCatalog.defaultAutoKey) private var defaultAuto = false
    @AppStorage(ModelCatalog.defaultAutoShipKey) private var defaultAutoShip = false
    @AppStorage(StatusIndicatorVisibility.storageKey) private var statusVisibilityRaw = StatusIndicatorVisibility.everywhere.rawValue
    @AppStorage(StatusIndicatorVisibility.chatIncidentOverrideKey) private var showStatusInChatOnIncident = true
    @AppStorage(ExternalAgentIntegration.mcp.defaultsKey)
    private var externalMCPModels = ExternalAgentIntegration.mcp.defaultModelSelection
    @AppStorage(ExternalAgentIntegration.skills.defaultsKey)
    private var externalSkillsModels = ExternalAgentIntegration.skills.defaultModelSelection
    @AppStorage(ExternalAgentIntegration.plugins.defaultsKey)
    private var externalPluginModels = ExternalAgentIntegration.plugins.defaultModelSelection
    @AppStorage(ACPAgent.genericExecutableDefaultsKey) private var genericACPExecutable = ""
    @AppStorage(ACPAgent.genericArgumentsDefaultsKey) private var genericACPArguments = ""

    /// Live install + auth state per provider, populated by an async probe on appearance and
    /// re-run whenever this tab comes back into view (so logging in/out elsewhere reflects).
    @State private var providers: [ProviderStatus] = []

    var body: some View {
        Form {
            Section("New chats") {
                Picker("Default model", selection: $defaultModel) {
                    ForEach(ModelCatalog.models, id: \.self) { sku in
                        (Text(ModelCatalog.displayName(sku))
                            + (ModelCatalog.contextBadge(for: sku).map { Text(" \($0)").foregroundColor(.secondary) } ?? Text(""))
                        ).tag(sku)
                    }
                }
                Picker("Default \(ModelCatalog.effortNoun(for: defaultModel).lowercased())", selection: $defaultEffort) {
                    ForEach(ModelCatalog.efforts(for: defaultModel), id: \.self) { Text(ModelCatalog.effortDisplayName($0)).tag($0) }
                }
                Toggle("Start new chats in Auto mode", isOn: $defaultAuto)
                Toggle("Autoship new chats", isOn: $defaultAutoShip)
            }

            Section("Orchestra") {
                Picker("Supervisor model", selection: $defaultSupervisorModel) {
                    ForEach(ModelCatalog.models, id: \.self) { sku in
                        (Text(ModelCatalog.displayName(sku))
                            + (ModelCatalog.contextBadge(for: sku).map { Text(" \($0)").foregroundColor(.secondary) } ?? Text(""))
                        ).tag(sku)
                    }
                }
                Picker("Worker model", selection: $defaultOrchestraWorkerModel) {
                    ForEach(ModelCatalog.models, id: \.self) { sku in
                        (Text(ModelCatalog.displayName(sku))
                            + (ModelCatalog.contextBadge(for: sku).map { Text(" \($0)").foregroundColor(.secondary) } ?? Text(""))
                        ).tag(sku)
                    }
                }
                Picker("Max concurrent workers", selection: $defaultOrchestraMaxWorkers) {
                    ForEach(ModelCatalog.orchestraMaxWorkerChoices, id: \.self) { count in
                        Text(count <= 0 ? "Unlimited" : "\(count)").tag(count)
                    }
                }
                .help("How many worker subagents the supervisor may run at once. Extra spawns wait for a free slot. The fan-out multiplies token usage, so raise this deliberately.")
            }

            Section("External integrations") {
                integrationModelsRow("MCP servers", selection: $externalMCPModels)
                integrationModelsRow("Skills", selection: $externalSkillsModels)
                integrationModelsRow("Plugins", selection: $externalPluginModels)
                Text(
                    "Use provider-configured integrations from your agent homes. Choose one or "
                        + "more models for each category. Skills retain their existing all-model "
                        + "behavior; MCP servers and plugins are explicit opt-ins.")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Section("Custom ACP agent") {
                TextField("Executable", text: $genericACPExecutable, prompt: Text("acp-agent"))
                    .textFieldStyle(.roundedBorder)
                TextField("Arguments", text: $genericACPArguments, prompt: Text("acp"))
                    .textFieldStyle(.roundedBorder)
                Text("Configure any command that speaks Agent Client Protocol over stdio. Arguments are split without invoking a shell; quotes and backslash escaping are supported.")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Section("Backend") {
                ForEach(providers) { provider in
                    HStack(spacing: 8) {
                        Circle()
                            .fill(provider.isConnected ? Color.green : Color.red)
                            .frame(width: 8, height: 8)
                        Text(provider.name)
                        Spacer()
                        Text(provider.isConnected ? "Connected" : "Not Connected")
                            .foregroundStyle(.secondary)
                    }
                    .help(provider.detail)
                }
            }
            .task(id: genericACPExecutable + "|" + genericACPArguments) {
                providers = await ProviderAvailability.probeAll()
            }

            Section("Service status") {
                Picker("Show status indicator", selection: $statusVisibilityRaw) {
                    ForEach(StatusIndicatorVisibility.allCases) { Text($0.label).tag($0.rawValue) }
                }
                Toggle("Show in chat during an incident on the current model's provider", isOn: $showStatusInChatOnIncident)
                    .disabled(StatusIndicatorVisibility(rawValue: statusVisibilityRaw) == .everywhere)
                ForEach(StatusProvider.allCases) { provider in
                    Toggle("Monitor \(provider.displayName)", isOn: providerBinding(provider))
                }
            }
        }
        .formStyle(.grouped)
        .onAppear {
            defaultEffort = ModelCatalog.clampedEffort(defaultEffort, for: defaultModel)
            pushDefaults()
        }
        .onChange(of: defaultModel) { _, _ in
            // A new default model may not support the stored effort (e.g. switching to Codex
            // drops "max" → "xhigh").
            defaultEffort = ModelCatalog.clampedEffort(defaultEffort, for: defaultModel)
            pushDefaults()
        }
        .onChange(of: defaultEffort) { _, _ in pushDefaults() }
        .onChange(of: defaultSupervisorModel) { _, _ in pushDefaults() }
        .onChange(of: defaultOrchestraWorkerModel) { _, _ in pushDefaults() }
        .onChange(of: defaultOrchestraMaxWorkers) { _, _ in pushDefaults() }
        .onChange(of: defaultAuto) { _, _ in pushDefaults() }
        .onChange(of: defaultAutoShip) { _, _ in
            if defaultAutoShip { defaultAuto = true }  // shipping implies autonomous
            pushDefaults()
        }
    }

    @ViewBuilder
    private func integrationModelsRow(_ title: String, selection: Binding<String>) -> some View {
        HStack {
            Text(title)
            Spacer()
            Menu(scopeLabel(selection.wrappedValue)) {
                Button("All models") { selection.wrappedValue = "*" }
                Button("No models") {
                    selection.wrappedValue = ExternalAgentIntegrationSettings.encodedModels([])
                }
                Divider()
                ForEach(ModelCatalog.models, id: \.self) { model in
                    Button {
                        toggleIntegrationModel(model, selection: selection)
                    } label: {
                        if scopeContains(model, raw: selection.wrappedValue) {
                            Label(ModelCatalog.displayName(model), systemImage: "checkmark")
                        } else {
                            Text(ModelCatalog.displayName(model))
                        }
                    }
                }
            }
            .fixedSize()
        }
    }

    private func scopeContains(_ model: String, raw: String) -> Bool {
        let models = ExternalAgentIntegrationSettings.models(from: raw)
        return models.contains("*") || models.contains(model)
    }

    private func scopeLabel(_ raw: String) -> String {
        let models = ExternalAgentIntegrationSettings.models(from: raw)
        if models.contains("*") { return "All models" }
        if models.isEmpty { return "No models" }
        if models.count == 1, let model = models.first {
            return ModelCatalog.displayName(model)
        }
        return "\(models.count) models"
    }

    private func toggleIntegrationModel(_ model: String, selection: Binding<String>) {
        var models = ExternalAgentIntegrationSettings.models(from: selection.wrappedValue)
        if models.contains("*") { models = Set(ModelCatalog.models) }
        if models.contains(model) {
            models.remove(model)
        } else {
            models.insert(model)
        }
        selection.wrappedValue = models == Set(ModelCatalog.models)
            ? "*" : ExternalAgentIntegrationSettings.encodedModels(models)
    }

    private func pushDefaults() {
        store.defaultModel = defaultModel
        store.defaultEffort = defaultEffort
        store.defaultSupervisorModel = defaultSupervisorModel
        store.defaultOrchestraWorkerModel = defaultOrchestraWorkerModel
        store.defaultOrchestraMaxConcurrentWorkers = defaultOrchestraMaxWorkers
        store.defaultAuto = defaultAuto
        store.defaultAutoShip = defaultAutoShip
    }

    /// Toggle binding for one provider's status monitoring: reads/writes the live store set
    /// (so the row reflects state immediately) and persists the choice for next launch.
    private func providerBinding(_ provider: StatusProvider) -> Binding<Bool> {
        Binding(
            get: { store.enabledStatusProviders.contains(provider) },
            set: { isOn in
                UserDefaults.standard.set(isOn, forKey: StatusIndicatorSettings.providerKey(provider))
                var enabled = store.enabledStatusProviders
                if isOn { enabled.insert(provider) } else { enabled.remove(provider) }
                store.setEnabledStatusProviders(enabled)
            })
    }
}

/// Usage: the subscription-limit gauges from the home dashboard, gathered into Settings so
/// they're checkable without leaving the panel. Both are the same cards the home view shows:
/// `QuotaCard` reads `AppStore.subscriptionUsage` (Claude) and `CodexUsageCard` reads
/// `AppStore.codexUsage` (Codex), each falling back to its own placeholder before data lands.
/// Not a `Form`: the pane shows the dashboard cards verbatim so the two views stay identical.
private struct UsageSettingsTab: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 20) {
                Text("The same account-wide subscription limits shown on the home dashboard.")
                    .settingsCaption()

                QuotaCard(title: "Claude")
                CodexUsageCard()
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(20)
        }
    }
}

/// Apple Intelligence model choice (used for summaries and auto-named chats), the
/// AI tool-call summarization options that depend on it, and its live availability
/// status. Summarization preferences sync to `AppStore` via `pushDefaults()`.
private struct IntelligenceSettingsTab: View {
    @Environment(AppStore.self) private var store

    @AppStorage(IntelligenceChoice.storageKey) private var choiceRaw = IntelligenceChoice.onDevice.rawValue
    @AppStorage(TranscriptDisplay.summarizeToolCallsKey) private var summarizeToolCalls = true
    @AppStorage(TranscriptDisplay.toolSummaryScopeKey) private var toolSummaryScopeRaw = ToolSummaryScope.bashOnly.rawValue

    private var choice: IntelligenceChoice { IntelligenceChoice(rawValue: choiceRaw) ?? .onDevice }
    private var toolSummaryScope: ToolSummaryScope { ToolSummaryScope(rawValue: toolSummaryScopeRaw) ?? .bashOnly }

    var body: some View {
        Form {
            Section("Model") {
                Picker("Model", selection: $choiceRaw) {
                    ForEach(IntelligenceChoice.allCases) { option in
                        // Energy Aware switches on AC vs. battery, so it's meaningless on a
                        // desktop — gray it out there (desktops have no battery to detect).
                        Text(option.label)
                            .tag(option.rawValue)
                            .disabled(option == .energyAware && !PowerSource.hasBattery)
                    }
                }
                .pickerStyle(.radioGroup)
            }

            Section("Tool call summaries") {
                Toggle("Summarize tool calls with AI", isOn: $summarizeToolCalls)
                if summarizeToolCalls {
                    Picker("AI summarizes", selection: $toolSummaryScopeRaw) {
                        Text("Bash commands only").tag(ToolSummaryScope.bashOnly.rawValue)
                        Text("All tool calls").tag(ToolSummaryScope.all.rawValue)
                    }
                }
            }

            Section("Status") {
                LabeledContent("On-device model", value: IntelligenceAvailability.status(for: .onDevice))
                LabeledContent("Private Cloud Compute", value: IntelligenceAvailability.status(for: .privateCloudCompute))
                if PowerSource.hasBattery {
                    LabeledContent("Energy Aware", value: IntelligenceAvailability.status(for: .energyAware))
                }
            }
            .font(.callout)
        }
        .formStyle(.grouped)
        .onAppear { pushDefaults() }
        .onChange(of: summarizeToolCalls) { _, _ in pushDefaults() }
        .onChange(of: toolSummaryScopeRaw) { _, _ in pushDefaults() }
    }

    private func pushDefaults() {
        store.summarizeToolCalls = summarizeToolCalls
        store.toolSummaryScope = toolSummaryScope
    }
}

/// The "Antimatter" tab: the meshing service, device pairing, and the sibling Macs meshed with
/// this one (host side).
private struct RemoteSettingsTab: View {
    var body: some View {
        Form {
            RemoteAccessSection()
        }
        .formStyle(.grouped)
    }
}

private extension View {
    /// Shared styling for the small explanatory captions beneath settings controls.
    func settingsCaption() -> some View {
        self.font(.caption)
            .foregroundStyle(.secondary)
            .fixedSize(horizontal: false, vertical: true)
    }
}

/// Canonical links into the public support site (`nucleic.blakeslee.xyz/support`). The full
/// how-to lives on the web, so a toggle keeps a one-line hint here and defers the detail —
/// setup steps, requirements, gotchas — to the matching article via `LearnMoreLink`.
private enum SupportURL {
    private static let base = "https://nucleic.blakeslee.xyz"
    static let home = URL(string: "\(base)/support.html")!
    static let nucleicControl = URL(string: "\(base)/support/nucleic-control.html")!
    static let sandboxes = URL(string: "\(base)/support/sandboxes.html")!
    static let virtualMachines = URL(string: "\(base)/support/virtual-machines.html")!
    static let macVMBaseImage = URL(string: "\(base)/support/macos-vm-base-image.html")!
    static let computerUse = URL(string: "\(base)/support/computer-use.html")!
}

/// A compact, caption-weight "Learn more" link to a support article. Sits directly under a
/// toggle's short hint, replacing the long inline instructions that used to clutter the UI.
private struct LearnMoreLink: View {
    private let label: String
    private let url: URL

    init(_ label: String = "Learn more", url: URL) {
        self.label = label
        self.url = url
    }

    var body: some View {
        Link(destination: url) {
            HStack(spacing: 3) {
                Text(label)
                Image(systemName: "arrow.up.right").imageScale(.small)
            }
            .font(.caption)
        }
        .fixedSize(horizontal: false, vertical: true)
    }
}
