← back to Exo
feat(macos-app): add custom namespace UI for cluster isolation
4f6fcd9e93d77e846e729801c3559a915f212c48 · 2025-12-24 22:36:55 +0530 · madanlalit
Add Advanced Options section with custom namespace field that allows
users to override EXO_LIBP2P_NAMESPACE environment variable. This
enables splitting machines that can see each other into separate
clusters.
- Added customNamespace property with UserDefaults persistence
- Added Advanced Options collapsible section with text field
- Added Save & Restart button that auto-restarts exo process
- Namespace replaces buildTag when custom value is set
- Falls back to buildTag (version) when namespace is empty
Files touched
M app/EXO/EXO/ContentView.swiftM app/EXO/EXO/ExoProcessController.swift
Diff
commit 4f6fcd9e93d77e846e729801c3559a915f212c48
Author: madanlalit <116655920+madanlalit@users.noreply.github.com>
Date: Wed Dec 24 22:36:55 2025 +0530
feat(macos-app): add custom namespace UI for cluster isolation
Add Advanced Options section with custom namespace field that allows
users to override EXO_LIBP2P_NAMESPACE environment variable. This
enables splitting machines that can see each other into separate
clusters.
- Added customNamespace property with UserDefaults persistence
- Added Advanced Options collapsible section with text field
- Added Save & Restart button that auto-restarts exo process
- Namespace replaces buildTag when custom value is set
- Falls back to buildTag (version) when namespace is empty
---
app/EXO/EXO/ContentView.swift | 45 ++++++++++++++++++++++++++++++++++
app/EXO/EXO/ExoProcessController.swift | 17 ++++++++++++-
2 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/app/EXO/EXO/ContentView.swift b/app/EXO/EXO/ContentView.swift
index 2b8ba2bd..75a23c12 100644
--- a/app/EXO/EXO/ContentView.swift
+++ b/app/EXO/EXO/ContentView.swift
@@ -20,6 +20,8 @@ struct ContentView: View {
@State private var showDebugInfo = false
@State private var bugReportInFlight = false
@State private var bugReportMessage: String?
+ @State private var showAdvancedOptions = false
+ @State private var pendingNamespace: String = ""
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -197,6 +199,8 @@ struct ContentView: View {
updater.checkForUpdates()
}
.padding(.bottom, 8)
+ advancedOptionsSection
+ .padding(.bottom, 8)
debugSection
.padding(.bottom, 8)
controlButton(title: "Quit", tint: .secondary) {
@@ -327,6 +331,47 @@ struct ContentView: View {
}
}
+ private var advancedOptionsSection: some View {
+ VStack(alignment: .leading, spacing: 6) {
+ HStack {
+ Text("Advanced Options")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ Spacer()
+ collapseButton(isExpanded: $showAdvancedOptions)
+ }
+ .animation(nil, value: showAdvancedOptions)
+ if showAdvancedOptions {
+ VStack(alignment: .leading, spacing: 8) {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Cluster Namespace")
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ HStack {
+ TextField("optional", text: $pendingNamespace)
+ .textFieldStyle(.roundedBorder)
+ .font(.caption2)
+ .onAppear {
+ pendingNamespace = controller.customNamespace
+ }
+ Button("Save & Restart") {
+ controller.customNamespace = pendingNamespace
+ if controller.status == .running || controller.status == .starting {
+ controller.restart()
+ }
+ }
+ .font(.caption2)
+ .disabled(pendingNamespace == controller.customNamespace)
+ }
+
+ }
+ }
+ .transition(.opacity)
+ }
+ }
+ .animation(.easeInOut(duration: 0.25), value: showAdvancedOptions)
+ }
+
private var debugSection: some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
diff --git a/app/EXO/EXO/ExoProcessController.swift b/app/EXO/EXO/ExoProcessController.swift
index ad25e5a5..431a6d68 100644
--- a/app/EXO/EXO/ExoProcessController.swift
+++ b/app/EXO/EXO/ExoProcessController.swift
@@ -2,6 +2,8 @@ import AppKit
import Combine
import Foundation
+private let customNamespaceKey = "EXOCustomNamespace"
+
@MainActor
final class ExoProcessController: ObservableObject {
enum Status: Equatable {
@@ -27,6 +29,13 @@ final class ExoProcessController: ObservableObject {
@Published private(set) var status: Status = .stopped
@Published private(set) var lastError: String?
@Published private(set) var launchCountdownSeconds: Int?
+ @Published var customNamespace: String = {
+ return UserDefaults.standard.string(forKey: customNamespaceKey) ?? ""
+ }() {
+ didSet {
+ UserDefaults.standard.set(customNamespace, forKey: customNamespaceKey)
+ }
+ }
private var process: Process?
private var runtimeDirectoryURL: URL?
@@ -180,7 +189,7 @@ final class ExoProcessController: ObservableObject {
private func makeEnvironment(for runtimeURL: URL) -> [String: String] {
var environment = ProcessInfo.processInfo.environment
environment["EXO_RUNTIME_DIR"] = runtimeURL.path
- environment["EXO_LIBP2P_NAMESPACE"] = buildTag()
+ environment["EXO_LIBP2P_NAMESPACE"] = computeNamespace()
var paths: [String] = []
if let existing = environment["PATH"], !existing.isEmpty {
@@ -217,6 +226,12 @@ final class ExoProcessController: ObservableObject {
}
return "dev"
}
+
+ private func computeNamespace() -> String {
+ let base = buildTag()
+ let custom = customNamespace.trimmingCharacters(in: .whitespaces)
+ return custom.isEmpty ? base : custom
+ }
}
struct RuntimeError: LocalizedError {
← 839b67f3 [feat] Add an option to disable the worker (#1091)
·
back to Exo
·
Fix Discord link in README.md. Fixes #1096 (#1097) 4963c331 →