Reusable SwiftUI pieces for the two things a LocalLM Lab app usually needs a settings screen for
— MCP servers and the model layer — built entirely on LocalLMLabSDKCore's public API,
no private access to Core's internals. Apache 2.0 licensed, same as the rest of this repo.
| File | What it provides |
|---|---|
MCPServerManagerObservable.swift |
An ObservableObject wrapper around Core's MCPServerManager, so SwiftUI views can observe connection/tool state changes. Also adds LocalizedError conformance to Core's MCPServerError — human-readable error text is a UI concern Core deliberately doesn't own. |
MCPServerPickerView.swift |
The main "add/list/reconnect/disconnect/remove" screen, all three MCP auth types (none, personal access token, OAuth). Lists each server's tools and resources with enable/disable checkboxes, prompts read-only, plus a "Save As…" export (NSSavePanel — the one AppKit-specific piece, kept out of Core) built on Core's MCPServerState.exportSummary(). |
MCPOAuthWaitingView.swift |
A waiting/spinner view shown while an OAuth sign-in is in flight in the system browser. |
MCPResourcesPromptsView.swift |
Read-only browsing of a session's enabled resources/resource templates/prompts, with an "Attach"/"Use" action per item that calls Core's readResource/getPrompt and hands the raw result to a host-supplied callback — no opinion on what "attach" means, same no-persistence-of-its-own precedent as the server picker. Exports MCPResourcesView and MCPPromptsView. |
All bind directly to lab.models (an @Observable ModelRegistry) — availability, install
state, and live download progress are observable properties, never polled.
| File | What it provides |
|---|---|
ModelPickerView.swift |
The local-model surface: registry.knownModels with an availability badge each (Ready / Not downloaded / Needs credential / Requires macOS 27), bound to a Binding<ModelID?>. When LocalLMLabSDKInference is linked, also a "Downloaded models" section — installed models + on-disk size, a live progress bar per in-flight download (registry.downloads), and an "Add from Hugging Face" field wired to registry.startDownload(_:). This is the ready-made version of the hand-rolled download-progress loop in the model-layer examples. Also exports ClaudeAuthField (a secure field for the Claude API key, handed to the host, not persisted). |
AIModelsSettingsView.swift |
The assembled "AI Models" panel: built-in families with live availability, then one ProviderSettingsSection per configured online provider, then an Add provider menu. Host owns [RemoteProviderDraft] (keys → Keychain) and the onSave / onRemove / onTest closures that map a draft to a RemoteModelProvider and call lab.models.replace(_:). No dependency on LocalLMLabSDKRemote — the closures are the seam. |
ProviderSettingsSection.swift |
One online-provider block — API-key field, Configured ✓ badge, per-model row editor, Enable web search toggle + Max searches stepper, Test connection button (one result per model). Usable standalone. |
RemoteProviderDraft.swift |
RemoteProviderDraft + RemoteProviderKind — the UI-facing shape the host maps to RemoteProviderConfig (~20 lines; see examples/model-switch's ProviderGlue.swift). .new(_:) deliberately ships no default model ids — the host prefills them. |
ProviderTestOutcome.swift |
The result type for Test connection: one ModelResult (modelId / ok / detail) per configured model, or a message when the check couldn't run. |
ModelOnboarding.swift |
ModelOnboardingView + ModelOnboardingModel — a Validate → Download → Pin stepper, one block per repo. A failed preflight names its stage and reason (failed at .trustPolicy — …) and nothing after it runs; the download shows progress and can be cancelled; the Pin step shows the resolved commit and, when the host passes the pin it ships (ModelOnboardingRequest.expectedRevision), verifies it matches. Works from ModelOnboardingSource(provider:) or (registry:) — Core's DownloadableModelProvider only. ModelPickerView's "Add from Hugging Face" row now uses it. |
ModelUpdates.swift |
ModelUpdateView (check → what changes → update → roll back, with a switching state and a pauseInference hook) and ModelVersionsView (versions on disk, what removing each would actually free, a guarded Remove). Provider-agnostic: they take plain value types (ModelUpdateOffer, ModelVersionRow) and closures (ModelUpdateActions), so the host adapts its provider in a few lines — see "Adapting MLXModelProvider" below. The wording follows who decides: .userChosen (you decide), .developerOffered (the developer vouches for a version), .fixed(reason:). |
ModelPickerView (local models + MLX download) and AIModelsSettingsView (online providers) are
separate surfaces today — a full "AI Models" panel composes both. Unifying them is on the list.
ModelOnboardingView needs nothing but Core. Give it what to onboard and where the work happens:
let onboarding = ModelOnboardingModel(
requests: [ModelOnboardingRequest(repoID: "mlx-community/Qwen2.5-0.5B-Instruct-4bit",
expectedRevision: shippedPin)], // optional: verify the commit
source: ModelOnboardingSource(registry: lab.models)) // or (provider: mlxProvider)
ModelOnboardingView(model: onboarding, onFinished: { installed in … })ModelUpdateView and ModelVersionsView are provider-agnostic on purpose. The SDK's update and cleanup
APIs live on MLXModelProvider in Inference (macOS 27), which Components doesn't link, so these views own
presentation and state and call closures you supply. The pause point matters: downloading the new version
doesn't disturb a running conversation, but switching can, so ModelUpdateModel.pauseInference is awaited
after the download and before the switch, and state == .switching tells your UI to refuse new requests.
examples/components-updates-demo drives every state of all three views from
simulated sources. examples/mlx-control-room shows the same flows against a real
MLXModelProvider: pin capture, check / update / roll back, a developer-moved built-in model, and cleanup of old
versions, with the download, pin and verification steps visible as they happen.
About forty lines. (Compiled and run against the real Inference module.) vouchedCommit is for a built-in
model: ask your own channel which commit you vouch for — the SDK moves a shipped pin only to a commit hash
you name, never to main, and cannot tell whether your channel is authentic. Leave it nil for a model the
user chose.
import LocalLMLabSDKComponents
import LocalLMLabSDKInference
@available(macOS 27.0, *)
extension ModelUpdateActions {
static func mlx(_ provider: MLXModelProvider, repoID: String,
vouchedCommit: (@Sendable () async throws -> String)? = nil) -> ModelUpdateActions {
ModelUpdateActions(
check: {
let check = try await provider.checkPinUpdate(repoID, to: try await vouchedCommit?())
return ModelUpdateOffer(
current: check.pinnedRevision, available: check.latestRevision,
changes: check.changes.map { change in
let kind: ModelFileChange.Kind
switch change.kind {
case .added: kind = .added
case .removed: kind = .removed
case .modified: kind = .modified
@unknown default: kind = .modified
}
return ModelFileChange(path: change.path, kind: kind, oldSize: change.oldSize, newSize: change.newSize)
})
},
apply: { revision, progress, beforeSwitch in
for try await event in provider.updatePin(repoID, to: revision, beforeSwitch: beforeSwitch) {
if case .progress(_, _, let fraction) = event { progress(fraction) }
}
})
}
}
@available(macOS 27.0, *)
extension ModelVersionsModel {
convenience init(provider: MLXModelProvider, repoID: String) {
self.init(
list: {
provider.snapshots(for: repoID).map {
ModelVersionRow(revision: $0.revision, isCurrent: $0.isCurrent, isComplete: $0.isComplete,
freesBytes: $0.exclusiveBytes, sharedBytes: $0.sharedBytes)
}
},
remove: { try provider.removeSnapshot(repoID, revision: $0.revision) })
}
}Requires an SDK build that has these APIs. ModelOnboardingView's Pin step reads
InstalledModel.resolvedRevision, and the adapter above uses MLXModelProvider's pin-update and snapshot
APIs. The 1.0.0-RC.1 release as re-published on 2026-09-18 includes them; an earlier copy of RC.1 does
not. If a build fails with a checksum mismatch or reads like old SDK code, the release assets were replaced in
place — run scripts/flush-sdk-cache.sh (SwiftPM caches binary artifacts by URL, not by content).
None of these views persist anything themselves. MCP state goes through the
MCPServerManagerObservable you own (manager.core.restore(from:) at launch); model-layer state
goes through lab.snapshot() / lab.restore(from:) plus your own Keychain for provider keys.
See docs/sdk-guide.md §11
for the narrative walkthrough, examples/components-demo/ for the
MCP views in a full app, and examples/model-switch/ for
AIModelsSettingsView.
This is a library, not an app — there's nothing to sign or package, just swift build/swift test. Package.swift builds against SDK 1.0.0-RC.1 by default, same as the example apps:
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift buildSet LOCALLM_SDK_VERSION in a shell to build against a different published release — see
Package.swift's defaultSDKVersion / knownSDKReleases. To actually see these views running,
build and run
examples/components-demo instead — it depends on this package as
source, so any local change here is picked up immediately.