nvrsion: Add: Merge base image sections with virtual machines sections, merge “Included apps” with Mac vm controls, and remove “Included apps” description.

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-09 01:10:53 -07:00
co-authored by nucleic
parent 92822df7e4
commit b8171b7f88
4 changed files with 677 additions and 328 deletions
+374 -318
View File
@@ -506,6 +506,8 @@ private struct MacVMSettingsTab: View {
@State private var observingBaseVM = false
/// Whether the "Advanced" power-user toggles under the service section are expanded.
@State private var serviceAdvancedExpanded = 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] = []
@@ -526,18 +528,17 @@ private struct MacVMSettingsTab: View {
/// 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. Each guest's
/// service + base-image controls sit in their own sections; cross-guest session defaults and
/// resources follow at the end so they read as "applies to both".
/// 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 {
sessionDefaultsSection
macOSServiceSection
macOSBaseImageSection
includedAppsSection
linuxServiceSection
linuxBaseImageSection
virtualizationEngineSection
computerUseSection
sessionDefaultsSection
macOSSection
linuxSection
resourcesSection
runningNowSection
}
@@ -547,6 +548,26 @@ private struct MacVMSettingsTab: View {
reconcileBaseImageChoice()
bundledApps = MacVMSettings.bundledAppPaths
}
.sheet(isPresented: $showingPackages) { LinuxPackagesSheet() }
}
// MARK: - Virtualization engine (which guests are available)
@ViewBuilder private var virtualizationEngineSection: some View {
Section("Virtualization engine") {
Toggle("Enable macOS virtual machines", isOn: $serviceEnabled)
.disabled(!supported)
Toggle("Enable Linux virtual machines", 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
@@ -560,376 +581,412 @@ private struct MacVMSettingsTab: View {
set: { exposeByDefault = $0; linuxExposeByDefault = $0 })
}
/// 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 })
}
@ViewBuilder private var sessionDefaultsSection: some View {
Section("Sandboxed sessions") {
Toggle("Expose virtual machines to sandboxed sessions by default", isOn: exposeBothBinding)
.disabled(!anyServiceEnabled || !supported)
Text("New sandboxed sessions get their own isolated macOS and Linux VMs (exec tools) "
+ "without asking. Applies to whichever guest services are enabled below.")
.settingsCaption()
}
.disabled(!supported)
}
// MARK: - macOS guest sections
// MARK: - macOS (base image + included apps)
@ViewBuilder private var macOSServiceSection: some View {
Section("macOS virtual machines") {
Toggle("Enable macOS VM service", isOn: $serviceEnabled)
.disabled(!supported)
if !supported {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange)
Text("Running macOS guests requires Apple silicon.").font(.caption)
}
} else {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Image(systemName: "internaldrive.fill").foregroundStyle(.orange)
Text("Keep at least 64 GB of free disk space before enabling — the base "
+ "image, restore image, and per-session VM disks add up fast.")
.font(.caption)
}
LearnMoreLink(url: SupportURL.macVMs)
}
Toggle("Kernel-level use", isOn: $computerUseByDefault)
.disabled(!serviceEnabled || !supported)
.onChange(of: computerUseByDefault) { _, isOn in
// Semantic understanding builds on kernel-level use turning the base off
// turns the dependent off too, so it can't linger enabled on its own.
if !isOn { axAgentEnabled = false }
}
// Descriptions stay visible whenever their toggle is so the user knows what each does
// before turning it on, not only after.
Text("Uses virtual drivers to work in the background, without opening a window, "
+ "capturing your mouse, or locking your keyboard.")
.settingsCaption()
if computerUseByDefault {
Toggle("Semantic understanding", isOn: $axAgentEnabled)
.disabled(!serviceEnabled || !supported)
Text("Improves the agent's understanding of human interfaces. Uses the guest's "
+ "software systems to read and act on the UI at the element level. "
+ "Increases token usage.")
.settingsCaption()
}
LearnMoreLink(url: SupportURL.macVMComputerUse)
DisclosureGroup("Advanced", isExpanded: $serviceAdvancedExpanded) {
Toggle("Always open VM guest display", isOn: $alwaysOpenGuestDisplay)
.disabled(!serviceEnabled || !supported)
}
@ViewBuilder private var macOSSection: some View {
Section("macOS virtual machines") {
if let baseOSVersion {
LabeledContent("Base image", value: "macOS \(baseOSVersion)")
}
}
@ViewBuilder private var macOSBaseImageSection: some View {
Section("Base image") {
if let baseOSVersion {
LabeledContent("Installed guest macOS", value: baseOSVersion)
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)
}
}
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")
}
}
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")
}
Text("Opens a live window of the base VM during the build — watch (or click into) a "
+ "build that's stuck, e.g. a first-boot Setup Assistant blocking the automated "
+ "provisioning.")
.settingsCaption()
} else {
Button {
Task { await runBaseBuild() }
} label: {
Label(
baseStatus?.installed == true
? "Rebuild / re-provision base image" : "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 baseStatus, baseStatus.installed {
baseStatusView(baseStatus)
Button(role: .destructive) {
confirmingBaseDelete = true
} label: {
Label("Delete base image…", systemImage: "trash")
}
.disabled(!supported || building || deleting)
Text("Removes the installed base so the next build reinstalls macOS from "
+ "scratch — unlike Rebuild, which only re-runs provisioning on the existing "
+ "install. Re-downloads the ~14 GB restore image.")
.settingsCaption()
} label: {
Image(systemName: "ellipsis")
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.fixedSize()
.disabled(!supported || building || deleting)
}
if let buildError {
Text(buildError).font(.caption).foregroundStyle(.red)
} else {
Button {
Task { await runBaseBuild() }
} label: {
Label("Build base image", systemImage: "square.and.arrow.down.on.square")
.padding(.vertical, 4)
}
Picker("Base macOS version", selection: $restoreImageChoice) {
ForEach(Self.ipswCatalog) { choice in
Text(choice.label).tag(choice.id)
}
}
.onChange(of: restoreImageChoice) { _, newValue in
applyIPSWChoice(newValue)
.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)
}
}
.disabled(!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.")
.onChange(of: restoreImageChoice) { _, newValue in
applyIPSWChoice(newValue)
}
includedAppsControls
DisclosureGroup("Advanced", isExpanded: $serviceAdvancedExpanded) {
Toggle("Always open VM guest display", isOn: $alwaysOpenGuestDisplay)
.disabled(!serviceEnabled || !supported)
}
}
.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.")
}
}
@ViewBuilder private var includedAppsSection: some View {
Section("Included apps") {
VStack(alignment: .leading, spacing: 10) {
Text("Drag in — or browse for — “.app” bundles to bake into the base image's "
+ "Applications folder, so every session VM has them. They're included when you "
+ "build the base — or add them to an existing base (or running VMs) with the "
+ "buttons below, no full rebuild needed.")
.settingsCaption()
if bundledApps.isEmpty {
HStack(spacing: 6) {
Image(systemName: "square.and.arrow.down")
.foregroundStyle(.secondary)
Text("Drop “.app” bundles here, or use “Add app…”.")
.font(.caption).foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 6)
} else {
ForEach(bundledApps, id: \.self) { path in
includedAppRow(path)
}
}
HStack {
Button {
chooseApps()
} label: {
Label("Add app…", systemImage: "plus")
}
if !bundledApps.isEmpty {
Spacer()
Text("^[\(bundledApps.count) app](inflect: true)")
.font(.caption).foregroundStyle(.secondary)
}
}
/// The "Included apps" controls a drop zone plus the import/copy actions merged inline into
/// the macOS section. No sub-header or blurb: the drop zone's placeholder is self-explanatory.
@ViewBuilder private var includedAppsControls: some View {
VStack(alignment: .leading, spacing: 10) {
if bundledApps.isEmpty {
HStack(spacing: 6) {
Image(systemName: "square.and.arrow.down")
.foregroundStyle(.secondary)
Text("Drop “.app” bundles here, or use “Add app…”.")
.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 baseStatus, baseStatus.installed {
Button {
Task { await addAppsToBase() }
} label: {
HStack(spacing: 6) {
if addingToBase { ProgressView().controlSize(.small) }
Label("Add to base image", systemImage: "square.and.arrow.down.on.square")
}
}
.disabled(addingToBase || building || deleting || bundledApps.isEmpty)
Text("Adds the apps above to the existing base image without a full rebuild: boots "
+ "the base briefly, copies them into its Applications folder, and powers it "
+ "off. New session VMs then include them. (Takes a minute or two.)")
.settingsCaption()
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)
Text("Also streams the apps into each already-running VM's Applications folder over "
+ "the guest agent (no restart). Existing copies are replaced.")
.settingsCaption()
if let appPushStatus {
Text(appPushStatus).font(.caption).foregroundStyle(.secondary)
}
.padding(.vertical, 6)
} else {
ForEach(bundledApps, id: \.self) { path in
includedAppRow(path)
}
}
.disabled(!serviceEnabled || !supported)
HStack {
Button {
chooseApps()
} label: {
Label("Add app…", systemImage: "plus")
}
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 baseStatus, baseStatus.installed {
Button {
Task { await addAppsToBase() }
} label: {
HStack(spacing: 6) {
if addingToBase { ProgressView().controlSize(.small) }
Label("Import", systemImage: "square.and.arrow.down.on.square")
}
.padding(.vertical, 4)
}
.disabled(addingToBase || building || deleting || bundledApps.isEmpty)
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)
}
}
}
// MARK: - Linux guest sections
@ViewBuilder private var linuxServiceSection: some View {
Section("Linux virtual machines") {
Toggle("Enable Linux VM service", isOn: $linuxServiceEnabled)
.disabled(!supported)
if !supported {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange)
Text("Running Linux guests requires Apple silicon.").font(.caption)
}
} else {
Text("A full GUI Linux guest per session (Ubuntu + a minimal Wayland desktop) — the "
+ "sibling of the macOS VM on the same Virtualization stack. Exec and computer use "
+ "work over the guest's virtio-gpu display + USB input, no in-guest permissions. "
+ "A separate opt-in with its own base-image build.")
.settingsCaption()
LearnMoreLink(url: SupportURL.macVMs)
}
Toggle("Kernel-level use", isOn: $linuxComputerUseByDefault)
.disabled(!linuxServiceEnabled || !supported)
.onChange(of: linuxComputerUseByDefault) { _, isOn in
// Semantic understanding builds on kernel-level use keep the dependent from
// lingering enabled once its base is turned off.
if !isOn { linuxAxAgentEnabled = false }
}
Text("Boots the guest with a virtio-gpu display + USB keyboard/pointer so the agent can "
+ "see and drive the Linux desktop host-side — no in-guest agent, TCC, or SIP.")
.settingsCaption()
if linuxComputerUseByDefault {
Toggle("Semantic understanding", isOn: $linuxAxAgentEnabled)
.disabled(!linuxServiceEnabled || !supported)
Text("Improves the agent's understanding of human interfaces. Uses the guest's "
+ "accessibility (AT-SPI) systems to read and act on the UI at the element "
+ "level. Increases token usage.")
.settingsCaption()
}
@ViewBuilder private var linuxSection: some View {
Section("Linux virtual machines") {
if let s = linuxBaseStatus, s.installed {
LabeledContent("Base image", value: linuxDistroLabel(s))
}
}
@ViewBuilder private var linuxBaseImageSection: some View {
Section("Linux base image") {
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)
}
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 the kernel + Ubuntu root filesystem, "
+ "assembling the disk in-guest, then installing the desktop + toolchain. It runs "
+ "in the background; you can keep working.")
.settingsCaption()
} else {
Button {
Task { await runLinuxBaseBuild() }
} label: {
Label(
linuxBaseStatus?.installed == true
? "Rebuild Linux base image" : "Build Linux base image",
systemImage: "square.and.arrow.down.on.square")
.padding(.vertical, 4)
}
.disabled(!linuxServiceEnabled || !supported || linuxBuilding || linuxDeleting || building)
if let linuxBaseStatus, linuxBaseStatus.installed {
if linuxBaseStatus.provisioned {
Label("Ready — Linux desktop + builds + computer use.",
systemImage: "checkmark.seal.fill")
.font(.caption).foregroundStyle(.green)
} else {
Label("Installed — desktop/toolchain provisioning incomplete; rebuild.",
systemImage: "checkmark.circle").font(.caption)
}
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")
}
.disabled(!supported || linuxBuilding || linuxDeleting)
} label: {
Image(systemName: "ellipsis")
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.fixedSize()
.disabled(!supported || linuxBuilding || linuxDeleting)
}
if let linuxBuildError {
Text(linuxBuildError).font(.caption).foregroundStyle(.red)
} 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)
}
.disabled(!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 "
+ "(re-downloads the kernel + root filesystem). Running sessions are unaffected.")
if let linuxBuildError {
Text(linuxBuildError).font(.caption).foregroundStyle(.red)
}
Button {
showingPackages = true
} label: {
Label("Packages…", systemImage: "shippingbox")
.padding(.vertical, 4)
}
}
.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)
Text("Which guest a general computer-use task uses when either desktop would do. "
+ "Mac-only work (Xcode, a macOS/iOS app) always uses the macOS VM; Linux is lighter "
+ "and boots faster, so it's the default. Agents still escalate from a container to a "
+ "VM only when the work genuinely needs one.")
.settingsCaption()
Toggle("Let agents create their own Linux containers", isOn: $agentContainersEnabled)
.disabled(!containerServiceEnabled)
Text("Exposes the lightweight `linux_container` tool so a sandboxed agent can spin up its "
+ "own throwaway Linux containers for isolated work — the cheapest rung below a VM. "
+ "Requires the container sandbox service.")
.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)
@@ -1986,10 +2043,9 @@ private enum SupportURL {
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 macVMs = URL(string: "\(base)/support/virtual-machines.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 macVMComputerUse = URL(string: "\(base)/support/macos-vm-computer-use.html")!
static let macVMAXAgent = URL(string: "\(base)/support/macos-vm-ax-agent.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
+6 -6
View File
@@ -208,19 +208,19 @@
<div class="sup-group">
<div class="g-head"><h2>Virtual Machines</h2></div>
<p class="g-sub">Give an agent a whole Mac of its own — a per-session macOS VM for Xcode, the simulators, and codesign, so parallel Mac builds never collide on your host.</p>
<p class="g-sub">Give an agent a whole machine of its own — a per-session macOS or Linux VM for building, testing, and computer use, so parallel work never collides on your host.</p>
<div class="art-grid">
<a class="art-card" href="/support/virtual-machines.html">
<div class="t">macOS virtual machines: overview<span class="arw" aria-hidden="true">&rarr;</span></div>
<p>What macOS VMs are, the hardware and disk requirements, how per-session VMs are cloned and torn down, and how many run at once.</p>
<div class="t">Virtual machines: overview<span class="arw" aria-hidden="true">&rarr;</span></div>
<p>What macOS and Linux VMs are, the hardware and disk requirements, how per-session VMs are cloned and torn down, and how many run at once.</p>
</a>
<a class="art-card" href="/support/macos-vm-base-image.html">
<div class="t">Building the base image<span class="arw" aria-hidden="true">&rarr;</span></div>
<p>The one-time golden base install: what the build does, choosing a guest macOS version, and pointing at a prebuilt bundle or a specific restore image.</p>
</a>
<a class="art-card" href="/support/macos-vm-computer-use.html">
<div class="t">Computer use for macOS VMs<span class="arw" aria-hidden="true">&rarr;</span></div>
<p>Let an agent see and drive the VM's screen — clicks, keys, and scrolling. The default path runs host-side with no in-guest setup and no permissions.</p>
<a class="art-card" href="/support/computer-use.html">
<div class="t">Computer use: the two kinds<span class="arw" aria-hidden="true">&rarr;</span></div>
<p>Let an agent see and drive a VM's screen. The two kinds — kernel-level use and semantic understanding — what each does and when to reach for it.</p>
</a>
<a class="art-card" href="/support/macos-vm-ax-agent.html">
<div class="t">Setting up AX-based computer use<span class="badge">Advanced</span><span class="arw" aria-hidden="true">&rarr;</span></div>
+287
View File
@@ -0,0 +1,287 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Computer use: the two kinds — Nucleic Support</title>
<meta name="description" content="The two kinds of computer use in Nucleic virtual machines — kernel-level use and semantic understanding — what each does, when to use them, and how they compare." />
<meta name="theme-color" content="#faf7f3" />
<meta name="robots" content="index, follow" />
<!-- atom mark, reused as favicon -->
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><g fill='none' stroke-width='7'><ellipse cx='50' cy='50' rx='17' ry='40' transform='rotate(80 50 50)' stroke='%237AA7DD'/><ellipse cx='50' cy='50' rx='17' ry='40' transform='rotate(20 50 50)' stroke='%23DF619B'/><ellipse cx='50' cy='50' rx='17' ry='40' transform='rotate(140 50 50)' stroke='%23C5826F'/></g></svg>" />
<link rel="preload" href="../fonts/space-grotesk.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="../fonts/jetbrains-mono.woff2" as="font" type="font/woff2" crossorigin />
<style>
@font-face{font-family:'Space Grotesk';font-style:normal;font-weight:400 700;font-display:swap;src:url(../fonts/space-grotesk.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400 500;font-display:swap;src:url(../fonts/jetbrains-mono.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
</style>
<style>
:root{
--ink:#181520;--ink-soft:#5b5560;--ink-faint:#8b8590;
--paper:#faf7f3;--panel:#fffdfb;--panel-2:#f3ede6;--line:#e6ded4;--line-2:#efe9e1;
--blue:#5e9bd8;--pink:#db5f97;--magenta:#c264ad;--rust:#c47e69;--green:#5fa775;--amber:#d7a23f;
--sans:"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
--mono:"JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
--maxw:1080px;--prose:760px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
margin:0;overflow-x:clip;font-family:var(--sans);color:var(--ink);background:var(--paper);
background-image:radial-gradient(var(--line) 1px, transparent 1px);
background-size:26px 26px;background-position:-13px -13px;
-webkit-font-smoothing:antialiased;line-height:1.5;
}
a{color:inherit}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px}
.mono{font-family:var(--mono)}
.kicker{font-family:var(--mono);font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-faint)}
/* ---------- top bar ---------- */
header{position:sticky;top:0;z-index:20;backdrop-filter:saturate(1.1) blur(8px);background:color-mix(in srgb, var(--paper) 82%, transparent);border-bottom:1px solid var(--line-2)}
.bar{display:flex;align-items:center;justify-content:space-between;height:62px}
.brand{display:flex;align-items:center;gap:11px;font-weight:600;letter-spacing:-.01em;font-size:17px;text-decoration:none;color:var(--ink)}
.brand .mk{width:26px;height:26px;display:block}
.nav{display:flex;align-items:center;gap:22px;font-size:14px;color:var(--ink-soft)}
.nav a{text-decoration:none}
.nav .lk:hover{color:var(--ink)}
.btn{font-family:var(--sans);font-size:14px;font-weight:500;text-decoration:none;cursor:pointer;border:0;padding:9px 16px;border-radius:10px;color:#fff;background:var(--ink);transition:transform .12s ease, opacity .12s ease;white-space:nowrap;-webkit-tap-highlight-color:transparent}
.btn:link,.btn:visited,.btn:hover,.btn:focus,.btn:active{color:#fff}
.btn:hover{opacity:.9;transform:translateY(-1px)}
@media (max-width:600px){ .nav .lk{display:none} }
/* ---------- doc header ---------- */
.crumbs{max-width:var(--prose);margin:0 auto;padding:26px 0 0;font-family:var(--mono);font-size:12px;color:var(--ink-faint);letter-spacing:.02em}
.crumbs a{text-decoration:none;color:var(--ink-soft)}
.crumbs a:hover{color:var(--ink)}
.crumbs .sep{color:var(--line);margin:0 8px}
.doc-hero{padding:16px 0 22px}
.doc-hero .wrap{max-width:var(--prose)}
.doc-hero h1{font-size:clamp(30px,4.6vw,44px);line-height:1.05;letter-spacing:-.03em;font-weight:600;margin:12px 0 0}
.doc-hero h1 .em{background:linear-gradient(100deg,var(--rust),var(--pink) 45%,var(--magenta) 70%,var(--blue));-webkit-background-clip:text;background-clip:text;color:transparent}
.doc-hero .lede{max-width:var(--prose);margin:16px 0 0;font-size:17px;color:var(--ink-soft);line-height:1.62}
/* ---------- on this page ---------- */
.toc{max-width:var(--prose);margin:22px auto 0;border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:16px 20px}
.toc h2{margin:0 0 10px;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-faint);font-weight:600;font-family:var(--mono)}
.toc ol{margin:0;padding-left:0;list-style:none;counter-reset:toc;display:grid;gap:7px}
.toc li{counter-increment:toc;position:relative;padding-left:28px;font-size:14.5px}
.toc li::before{content:counter(toc,decimal-leading-zero);position:absolute;left:0;top:1px;font-family:var(--mono);font-size:11px;color:var(--ink-faint)}
.toc a{text-decoration:none;color:var(--ink-soft)}
.toc a:hover{color:var(--ink)}
/* ---------- prose ---------- */
main{padding:10px 0 30px}
.prose{max-width:var(--prose);margin:0 auto}
.prose section{padding:28px 0;border-top:1px solid var(--line-2)}
.prose h2{font-size:22px;font-weight:600;letter-spacing:-.02em;margin:0 0 6px;scroll-margin-top:80px}
.prose h2 .n{font-family:var(--mono);font-size:13px;color:var(--ink-faint);margin-right:10px;font-weight:500}
.prose h3{font-size:16px;font-weight:600;letter-spacing:-.01em;margin:22px 0 6px;color:var(--ink)}
.prose p{margin:12px 0;color:var(--ink-soft);font-size:15.5px;line-height:1.65}
.prose ul{margin:12px 0;padding-left:22px;color:var(--ink-soft);font-size:15.5px;line-height:1.65}
.prose li{margin:7px 0}
.prose strong{color:var(--ink);font-weight:600}
.prose a{color:var(--ink);text-decoration:underline;text-decoration-color:var(--line);text-underline-offset:3px}
.prose a:hover{text-decoration-color:var(--magenta)}
.prose code{font-family:var(--mono);font-size:.86em;background:var(--panel-2);border:1px solid var(--line-2);border-radius:5px;padding:1px 6px}
.prose .lede{font-size:17px;color:var(--ink);line-height:1.6}
.kbd{font-family:var(--mono);font-size:.82em;background:var(--panel);border:1px solid var(--line);border-bottom-width:2px;border-radius:6px;padding:1px 7px;color:var(--ink);white-space:nowrap}
.ui{font-weight:600;color:var(--ink)}
/* two-path table */
.ftable{width:100%;border-collapse:collapse;margin:16px 0;font-size:14px}
.ftable th,.ftable td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line-2);vertical-align:top}
.ftable th{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint);font-weight:600}
.ftable td:first-child{font-weight:600;color:var(--ink);white-space:nowrap}
.ftable td:last-child{color:var(--ink-soft)}
/* callouts */
.callout{border-left:3px solid var(--line);border-radius:0 10px 10px 0;padding:14px 16px;margin:18px 0;font-size:14.5px;color:var(--ink-soft);line-height:1.6}
.callout strong{color:var(--ink)}
.callout code{font-family:var(--mono);font-size:.86em;background:var(--panel-2);border:1px solid var(--line-2);border-radius:5px;padding:1px 6px}
.callout.note{border-color:color-mix(in srgb,var(--blue) 60%,var(--line));background:color-mix(in srgb,var(--blue) 6%,var(--panel))}
.callout.tip{border-color:color-mix(in srgb,var(--green) 60%,var(--line));background:color-mix(in srgb,var(--green) 6%,var(--panel))}
.callout.warn{border-color:color-mix(in srgb,var(--amber) 70%,var(--line));background:color-mix(in srgb,var(--amber) 8%,var(--panel))}
.callout .lbl{display:block;font-size:11px;letter-spacing:.1em;text-transform:uppercase;font-family:var(--mono);margin-bottom:4px}
.callout.note .lbl{color:var(--blue)}
.callout.tip .lbl{color:var(--green)}
.callout.warn .lbl{color:color-mix(in srgb,var(--amber) 80%,var(--ink))}
/* numbered steps */
.steps{counter-reset:step;list-style:none;padding:0;margin:16px 0}
.steps>li{counter-increment:step;position:relative;padding:0 0 20px 46px;margin:0}
.steps>li::before{content:counter(step);position:absolute;left:0;top:-2px;width:30px;height:30px;border-radius:9px;background:var(--ink);color:#fff;font-family:var(--mono);font-size:13px;display:flex;align-items:center;justify-content:center}
.steps>li:not(:last-child)::after{content:"";position:absolute;left:14px;top:32px;bottom:6px;width:2px;background:var(--line)}
.steps>li h3{margin:4px 0 4px;font-size:16px}
.steps>li p{margin:6px 0}
/* code block */
pre{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;overflow-x:auto;margin:14px 0}
pre code{font-family:var(--mono);font-size:13px;color:var(--ink);line-height:1.7;background:none;border:0;padding:0}
pre code .c{color:var(--ink-faint)}
/* next / related */
.related{max-width:var(--prose);margin:0 auto;padding:26px 0 0;border-top:1px solid var(--line-2)}
.related h2{font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-faint);font-weight:600;font-family:var(--mono);margin:0 0 12px}
.related a{display:flex;align-items:center;gap:9px;text-decoration:none;color:var(--ink);border:1px solid var(--line);border-radius:12px;padding:13px 16px;margin:8px 0;font-weight:500;font-size:15px;transition:border-color .14s ease}
.related a:hover{border-color:color-mix(in srgb,var(--magenta) 45%,var(--line))}
.related a .arw{margin-left:auto;color:var(--ink-faint)}
footer{border-top:1px solid var(--line-2);padding:30px 0 50px;margin-top:24px}
.foot{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap;color:var(--ink-faint);font-size:13px}
.foot .mono{font-size:12px}
.foot-links{display:flex;gap:18px;flex-wrap:wrap}
.foot-links a{text-decoration:none;color:var(--ink-soft)}
.foot-links a:hover{color:var(--ink)}
.dotsep{color:var(--line)}
</style>
</head>
<body>
<!-- ===== top bar ===== -->
<header>
<div class="wrap bar">
<a class="brand" href="/">
<svg class="mk" viewBox="0 0 100 100" aria-hidden="true">
<g fill="none" stroke-width="7">
<ellipse cx="50" cy="50" rx="17" ry="40" transform="rotate(80 50 50)" stroke="#7AA7DD"/>
<ellipse cx="50" cy="50" rx="17" ry="40" transform="rotate(20 50 50)" stroke="#DF619B"/>
<ellipse cx="50" cy="50" rx="17" ry="40" transform="rotate(140 50 50)" stroke="#C5826F"/>
</g>
</svg>
Nucleic
</a>
<nav class="nav">
<a class="btn" href="/">Home</a>
</nav>
</div>
</header>
<div class="wrap">
<nav class="crumbs" aria-label="Breadcrumb">
<a href="/support.html">Support</a><span class="sep">/</span>
<a href="/support/virtual-machines.html">Virtual Machines</a><span class="sep">/</span>
Computer use
</nav>
</div>
<!-- ===== title ===== -->
<section class="doc-hero">
<div class="wrap">
<div class="kicker">Virtual Machines</div>
<h1>Computer use: the <span class="em">two kinds</span></h1>
<p class="lede">Beyond running commands, an agent can see and operate a virtual machine's screen. Nucleic offers this in two forms — <strong>kernel-level use</strong> and <strong>semantic understanding</strong> — that stack: the second builds on the first. This guide explains what each does and when to reach for it.</p>
</div>
</section>
<div class="wrap">
<nav class="toc" aria-label="On this page">
<h2>On this page</h2>
<ol>
<li><a href="#what">What computer use is</a></li>
<li><a href="#kernel">Kernel-level use</a></li>
<li><a href="#semantic">Semantic understanding</a></li>
<li><a href="#compare">Which to use</a></li>
<li><a href="#enable">Turning them on</a></li>
</ol>
</nav>
</div>
<main>
<div class="wrap prose">
<section id="what">
<h2><span class="n">01</span>What computer use is</h2>
<p class="lede">Computer use lets an agent <strong>see</strong> and drive a virtual machine's screen — clicking, typing, and scrolling through its interface — rather than only running commands.</p>
<p>It's the visual counterpart to running commands in the VM. Where commands are for scripting and builds, computer use is for anything with a screen. Reach for it when the agent needs to:</p>
<ul>
<li>Look at or click through an app it just built, to confirm the interface actually behaves.</li>
<li>Drive a tool's window — the parts you can't reach from the command line.</li>
<li>Check how something renders — a layout, a web page, a window — by taking an actual screenshot.</li>
</ul>
<p>Nucleic offers computer use in two forms. They <strong>stack</strong>: semantic understanding is an add-on to kernel-level use, not a replacement for it. Both are available for macOS and Linux virtual machines alike.</p>
</section>
<section id="kernel">
<h2><span class="n">02</span>Kernel-level use</h2>
<p>Kernel-level use is the foundation. The agent works the machine <strong>from the outside</strong>, the way a person at the keyboard would: it takes a screenshot of the screen, then moves the pointer, clicks, scrolls, and types.</p>
<p>Through it, an agent can:</p>
<ul>
<li><strong>Take a screenshot</strong> of the screen.</li>
<li><strong>Move and click the pointer</strong>, including press-and-drag.</li>
<li><strong>Scroll</strong> and <strong>type text</strong>, with modifier keys for shortcuts.</li>
<li><strong>Launch apps</strong> in the machine.</li>
</ul>
<p>These compose into a simple <strong>screenshot → decide → act</strong> loop: capture the screen, choose an action, perform it, then capture again to see what changed. It works quietly in the background — no window opens on your Mac, and it never captures your own mouse or keyboard. There's nothing to set up: it's ready as soon as a base image is built.</p>
</section>
<section id="semantic">
<h2><span class="n">03</span>Semantic understanding</h2>
<p>Semantic understanding layers on top of kernel-level use to give the agent a <strong>structured view of the interface</strong> — the on-screen controls as named elements rather than just pixels. With it, the agent can:</p>
<ul>
<li><strong>Act on a control by identity</strong> — press a specific button by its name — instead of guessing and clicking coordinates.</li>
<li><strong>Read the interface's structure</strong> — the controls present, their values, and what's focused — when it needs to understand a screen precisely.</li>
</ul>
<p>This makes GUI automation far more reliable, since the agent targets controls by what they are rather than where they happen to sit. The trade-off is that reading and reasoning over the interface's structure <strong>increases token usage</strong>, so it's worth turning on when you specifically want robust, by-identity control.</p>
</section>
<section id="compare">
<h2><span class="n">04</span>Which to use</h2>
<table class="ftable">
<thead><tr><th>Kind</th><th>What you get</th></tr></thead>
<tbody>
<tr>
<td>Kernel-level use</td>
<td><strong>See and drive the screen</strong> with screenshots, clicks, and typing. Works in the background with no setup. The right default for most GUI work.</td>
</tr>
<tr>
<td>Semantic understanding</td>
<td>Adds a <strong>structured, by-identity</strong> view of the interface for more robust automation — at the cost of higher token usage. An add-on to kernel-level use, not a replacement.</td>
</tr>
</tbody>
</table>
<div class="callout tip"><span class="lbl">Rule of thumb</span>Start with kernel-level use. Turn on semantic understanding when you want the agent to act on controls by identity or to read an interface precisely.</div>
</section>
<section id="enable">
<h2><span class="n">05</span>Turning them on</h2>
<p>In <span class="ui">Settings ▸ Virtual Machines</span>, under <span class="ui">Computer use</span>, turn on <span class="ui">Kernel-level use</span>. Once it's on, <span class="ui">Semantic understanding</span> appears beneath it — turn that on too if you want the structured view. Both apply to whichever virtual machines you've enabled.</p>
<p>Computer use runs against a built virtual machine, so you'll need a base image first — see <a href="/support/virtual-machines.html">Virtual machines: overview</a>.</p>
</section>
</div>
<div class="wrap related">
<h2>Related guides</h2>
<a href="/support/virtual-machines.html">Virtual machines: overview<span class="arw" aria-hidden="true">&rarr;</span></a>
<a href="/support/macos-vm-base-image.html">Building the base image<span class="arw" aria-hidden="true">&rarr;</span></a>
</div>
</main>
<footer>
<div class="wrap foot">
<div class="brand" style="font-size:15px">
<svg class="mk" viewBox="0 0 100 100" aria-hidden="true">
<g fill="none" stroke-width="7">
<ellipse cx="50" cy="50" rx="17" ry="40" transform="rotate(80 50 50)" stroke="#7AA7DD"/>
<ellipse cx="50" cy="50" rx="17" ry="40" transform="rotate(20 50 50)" stroke="#DF619B"/>
<ellipse cx="50" cy="50" rx="17" ry="40" transform="rotate(140 50 50)" stroke="#C5826F"/>
</g>
</svg>
Nucleic
</div>
<div class="foot-links">
<a href="/">Home</a>
<a href="/support.html">Support</a>
<a href="/privacy.html">Privacy</a>
<a href="/terms.html">Terms</a>
</div>
<div class="mono">© 2026 Andrew Blakeslee&nbsp;Moore · Made with Nucleic</div>
</div>
</footer>
</body>
</html>
+10 -4
View File
@@ -3,8 +3,8 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>macOS virtual machines: overview — Nucleic Support</title>
<meta name="description" content="Nucleic's macOS VMs give each agent session its own isolated Mac for Xcode, the simulators, and codesign — so parallel Mac builds never collide on your host." />
<title>Virtual machines: overview — Nucleic Support</title>
<meta name="description" content="Nucleic virtual machines give each agent session its own isolated macOS or Linux machine — for building, testing, and computer use — so parallel work never collides on your host." />
<meta name="theme-color" content="#faf7f3" />
<meta name="robots" content="index, follow" />
@@ -171,8 +171,8 @@
<section class="doc-hero">
<div class="wrap">
<div class="kicker">Virtual Machines</div>
<h1>macOS <span class="em">virtual machines</span></h1>
<p class="lede">Give an agent a whole Mac of its own — a per-session macOS VM for Xcode, the simulators, and codesign, so parallel Mac builds never collide on your host.</p>
<h1>Virtual <span class="em">machines</span></h1>
<p class="lede">Give an agent a whole machine of its own — a per-session macOS or Linux VM for building, testing, and computer use, so parallel work never collides on your host.</p>
</div>
</section>
@@ -198,6 +198,12 @@
<p class="lede">Each macOS VM is a per-session, isolated Mac running on Apple's Virtualization framework — a real, disposable macOS instance where <code>xcodebuild</code>, the simulators (<code>xcrun simctl</code>), and <code>codesign</code> run end-to-end without ever touching your host.</p>
<p>They exist to remove shared-host toolchain thrash. Two agents building on the same host collide and corrupt each other's derived-data build output — one clobbers the other's intermediate files mid-compile, and both builds fail in confusing ways. When each agent gets its own isolated Mac, concurrent builds never collide: every session compiles, simulates, and signs in its own clean environment.</p>
<p>This is exposed to agents as the <code>mac_vm_exec</code> tool — the per-agent alternative to the shared-host <code>host_exec</code>. Where <code>host_exec</code> runs commands directly on your Mac (shared by every session), <code>mac_vm_exec</code> runs them inside that session's own VM.</p>
<p>Nucleic offers the same idea in two flavors, each enabled independently in <span class="ui">Settings ▸ Virtual Machines</span>:</p>
<ul>
<li><strong>macOS virtual machines</strong> — a per-session Mac for Xcode, the simulators, and codesign.</li>
<li><strong>Linux virtual machines</strong> — a per-session Linux desktop, lighter and faster to start, for general build, test, and GUI work that doesn't need a Mac.</li>
</ul>
<p>Both build once into a reusable base image that every session is cloned from, and both support <a href="/support/computer-use.html">computer use</a> so an agent can see and drive the screen. The rest of this page uses macOS as the running example; the Linux flow mirrors it.</p>
</section>
<section id="requirements">