Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,11 @@ llama.cpp/
llama.cpp-rpc-b2b/
target/
llama.cpp.bak/

# macOS Swift menu bar app local build/config artifacts
macos/.build/
macos/.swiftpm/
macos/Package.resolved
macos/dist/
macos/.DS_Store
macos/Sources/.DS_Store
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ Or join without a GPU:
mesh-llm --client --join <token>
```

## macOS Menu Bar App

This repo also contains a native macOS menu bar controller app in `macos/`.
It bundles `mesh-llm` (plus `rpc-server`/`llama-server`) into a distributable
`Mesh LLM.app`.

Build and package from this repo root:

```bash
cd macos
./scripts/package-app.sh
```

Output:

```bash
macos/dist/Mesh LLM.app
```

## How it works
A common question is around latency and networks, there are a few ways that are addressed.
This uses mesh tech (quic) to distribute inference workload:
Expand Down
3 changes: 3 additions & 0 deletions macos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.build/
dist/
external/decentralized-inference/mesh-llm/.artifacts/
21 changes: 21 additions & 0 deletions macos/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// swift-tools-version: 5.10
import PackageDescription

let package = Package(
name: "MeshLLMMenuBar",
platforms: [
.macOS(.v13)
],
products: [
.executable(name: "MeshLLMMenuBar", targets: ["MeshLLMMenuBar"])
],
targets: [
.executableTarget(
name: "MeshLLMMenuBar",
path: "Sources",
resources: [
.copy("Resources")
]
)
]
)
89 changes: 89 additions & 0 deletions macos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Mesh LLM macOS Menu Bar App

This directory contains the macOS menu bar app in Swift that controls `mesh-llm` directly (no virtualization).

## Current Status
- Menu bar item with actions:
- `Start Mesh`
- `Stop Mesh`
- `Restart Mesh`
- `Copy Token`
- `Open Console`
- `Open Logs`
- `Settings...`
- `Quit`
- State-driven enable/disable behavior for menu actions
- `mesh-llm` process manager wired to start/stop/restart a local binary
- Status icon tint changes by state (running/error/transition/stopped) using the same base icon
- App packaging bundles `mesh-llm` into `.app/Contents/Resources/mesh-llm`
- Real health/token flow via local endpoints (`/health`, console `/api/status`)
- Writes logs to `~/Library/Logs/MeshLLMMenuBar/mesh-llm.log` and can open them in Console.app
- Startup waits for mesh health and may take a long time on first run while model download/loading completes

## Build (dev)
```bash
swift build
```

## Run (dev)
```bash
swift run MeshLLMMenuBar
```

The app runs as an accessory app (menu bar only).

## Runtime Configuration
Configure via `Settings...` in the menu.

Defaults:
- Model: `Qwen2.5-32B-Instruct-Q4_K_M`
- API port: `9337`
- Console port: `3131`

Environment variables still override settings:

- `MESH_LLM_MODEL_PATH` (path to GGUF/model arg for `--model`)
- `MESH_LLM_JOIN_TOKEN` (token for `--join`)
- `MESH_LLM_API_PORT` (default `9337`)
- `MESH_LLM_CONSOLE_PORT` (default `3131`, needed for token fetch)

Example:
```bash
MESH_LLM_MODEL_PATH="$HOME/.models/GLM-4.7-Flash-Q4_K_M.gguf" \
MESH_LLM_API_PORT=9337 \
MESH_LLM_CONSOLE_PORT=3131 \
swift run MeshLLMMenuBar
```

## Build + Bundle App With mesh-llm
This is the main workflow for distributable app packaging:

From `external/decentralized-inference/macos`:

```bash
./scripts/package-app.sh
```

From `external/decentralized-inference`:

```bash
cd macos
./scripts/package-app.sh
```

What this does:
1. Runs `../mesh-llm/scripts/build-mesh-binary.sh`
2. Builds release binary (`swift build -c release`)
3. Creates `dist/Mesh LLM.app` and bundles `mesh-llm` into `Contents/Resources`
4. Bundles `rpc-server` and `llama-server` from `../llama.cpp/build/bin`
5. Bundles app icon from `Sources/Resources/MeshLLM.icns`

## Project Layout
- `Sources/App`: app entry + app state
- `Sources/MenuBar`: menu bar controller and action wiring
- `Sources/Persistence`: settings storage + defaults
- `Sources/Process`: mesh process manager and bundled binary discovery
- `Sources/Mesh`: health/token provider
- `Sources/Utilities`: utility services (clipboard)
- `scripts/package-app.sh`: packaging pipeline (build mesh + build app + bundle mesh)
- `../mesh-llm/scripts/build-mesh-binary.sh`: local mesh binary build contract
208 changes: 208 additions & 0 deletions macos/Sources/App/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import AppKit

final class AppDelegate: NSObject, NSApplicationDelegate {
private var menuBarController: MenuBarController?
private var settingsWindowController: SettingsWindowController?
private var meshManager: MeshProcessManager?
private let settingsStore = MeshSettingsStore()
private let stateStore = AppStateStore()
private let logService = LogService()
private let loginItemService = LoginItemService()
private var startupModelDisplayName: String = MeshDefaults.model

func applicationDidFinishLaunching(_ notification: Notification) {
logService.append("MeshLLMMenuBar launched")
let settings = settingsStore.load()
let runtime = makeRuntime(from: settings)
startupModelDisplayName = runtime.modelDisplayName
stateStore.configurationError = settings.validationError
do {
try loginItemService.apply(enabled: settings.startOnLogin)
} catch {
logService.append("Failed to apply login item setting: \(error.localizedDescription)")
}
self.meshManager = runtime.meshManager
let clipboardService = ClipboardService()
menuBarController = MenuBarController(
stateStore: stateStore,
meshManager: runtime.meshManager,
tokenProvider: runtime.tokenProvider,
clipboardService: clipboardService,
consoleURL: runtime.consoleURL,
openLogsHandler: { [weak self] in self?.openLogs() },
openSettingsHandler: { [weak self] in self?.openSettingsWindow() }
)
}

func applicationWillTerminate(_ notification: Notification) {
logService.append("MeshLLMMenuBar terminating")
try? meshManager?.stop()
}

private func openSettingsWindow() {
let controller = SettingsWindowController(settingsStore: settingsStore) { [weak self] settings in
guard let self else { return }
self.applySettings(settings)
}
settingsWindowController = controller
controller.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}

private func openLogs() {
logService.openInConsoleApp()
}

private func handleMeshOutputLine(_ line: String) {
let isTransitioning = stateStore.meshStatus == .starting || stateStore.meshStatus == .restarting
let lower = line.lowercased()
if line.hasPrefix("Invite token:") {
let token = line.replacingOccurrences(of: "Invite token:", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
if !token.isEmpty {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.stateStore.token = token
self.menuBarController?.refresh()
}
}
}

if lower.contains("error:") {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.stateStore.meshStatus = .error
self.stateStore.startupDetail = nil
self.stateStore.lastErrorMessage = line
self.menuBarController?.refresh()
}
return
}

guard isTransitioning else { return }

var detail: String?
var shouldMarkRunning = false
if lower.contains("download") {
detail = "Downloading \(startupModelDisplayName)..."
} else if lower.contains("waiting for peers") || lower.contains("waiting for peer") {
detail = "Waiting for peers..."
shouldMarkRunning = true
} else if lower.contains("ready") || lower.contains("llama-server ready") {
detail = "Finishing startup..."
shouldMarkRunning = true
}

guard let detail else { return }
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if shouldMarkRunning {
self.stateStore.meshStatus = .running
}
self.stateStore.startupDetail = detail
self.menuBarController?.refresh()
}
}

private func handleMeshOutputText(_ text: String) {
_ = text
}

private func handleMeshProgressPercent(_ percent: String) {
guard stateStore.meshStatus == .starting || stateStore.meshStatus == .restarting else { return }
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let next = "Downloading \(self.startupModelDisplayName) \(percent)%"
if self.stateStore.startupDetail != next {
self.stateStore.startupDetail = next
self.menuBarController?.refresh()
}
}
}

private static func displayModelName(from modelPathOrName: String?) -> String {
guard let raw = modelPathOrName?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
return MeshDefaults.model
}
let last = URL(fileURLWithPath: raw).lastPathComponent
return last.isEmpty ? raw : last
}

private func makeRuntime(from settings: MeshRuntimeSettings) -> (meshManager: MeshProcessManager, tokenProvider: HTTPTokenProvider, consoleURL: URL, modelDisplayName: String) {
let meshManager = MeshProcessManager(
launchArguments: settings.launchArguments(),
logService: logService
)
meshManager.onOutputLine = { [weak self] line in
self?.handleMeshOutputLine(line)
}
meshManager.onOutputText = { [weak self] text in
self?.handleMeshOutputText(text)
}
meshManager.onProgressPercent = { [weak self] percent in
self?.handleMeshProgressPercent(percent)
}

let tokenProvider = HTTPTokenProvider(
apiBaseURL: URL(string: "http://127.0.0.1:\(settings.apiPort)")!,
consoleBaseURL: URL(string: "http://127.0.0.1:\(settings.consolePort)")!
)
return (
meshManager,
tokenProvider,
URL(string: "http://127.0.0.1:\(settings.consolePort)")!,
Self.displayModelName(from: settings.modelPath)
)
}

private func applySettings(_ settings: MeshRuntimeSettings) {
let wasRunning = stateStore.meshStatus == .running
try? meshManager?.stop()

let runtime = makeRuntime(from: settings)
startupModelDisplayName = runtime.modelDisplayName
meshManager = runtime.meshManager
do {
try loginItemService.apply(enabled: settings.startOnLogin)
} catch {
logService.append("Failed to apply login item setting: \(error.localizedDescription)")
}
stateStore.configurationError = settings.validationError
stateStore.lastErrorMessage = nil
stateStore.startupDetail = nil

menuBarController?.updateRuntime(
meshManager: runtime.meshManager,
tokenProvider: runtime.tokenProvider,
consoleURL: runtime.consoleURL
)

guard wasRunning, settings.validationError == nil else {
stateStore.meshStatus = .stopped
menuBarController?.refresh()
return
}

Task { @MainActor in
stateStore.meshStatus = .starting
stateStore.startupDetail = "Applying settings..."
menuBarController?.refresh()
do {
try runtime.meshManager.start()
try await runtime.tokenProvider.waitForReady()
stateStore.meshStatus = .running
stateStore.startupDetail = nil
do {
stateStore.token = try await runtime.tokenProvider.fetchToken()
} catch {
stateStore.token = nil
stateStore.lastErrorMessage = "Mesh restarted but token unavailable: \(error.localizedDescription)"
}
} catch {
stateStore.meshStatus = .error
stateStore.startupDetail = nil
stateStore.lastErrorMessage = "Failed to apply settings: \(error.localizedDescription)"
}
menuBarController?.refresh()
}
}
}
Loading