From 1d1e1f9030b4df0666d9183ff4300ed0c8fbb531 Mon Sep 17 00:00:00 2001 From: Bobby Vensko Date: Mon, 31 Aug 2026 18:10:29 -0400 Subject: [PATCH 1/3] fix: resolve uv from all standard locations, not just ~/.local/bin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Prompt LLM settings pane reported "uv not found at ~/.local/bin/uv" on machines where uv came from Homebrew. GemmaChatRunner.uvPath was a hardcoded `NSHomeDirectory() + "/.local/bin/uv"`, and the settings view duplicated that same literal — so a perfectly good /opt/homebrew/bin/uv was invisible to captions and the Scenario Generator. The codebase already knew how to find uv in two other places (MfluxInstaller.resolveUv and BinaryDetector.detect). Three independently-maintained candidate lists is how this drifted, so consolidate on one: UvInstaller.resolvedPath, which prefers the app-managed install and falls back to BinaryDetector.detect("uv"). - GemmaChatRunner.uvPath is now computed off it (not a `static let`, so the settings indicator notices a uv installed after launch), and the resolved path is bound once per run and threaded into spawn. - MfluxInstaller.resolveUv delegates instead of keeping its own list. - The four "uv not found at ~/.local/bin/uv" strings drop the path, which is no longer the only place checked; the settings row now shows which uv is actually in play. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TD3QGYw3rGwSPsDpbjp3Kh --- Utilities/GemmaChatRunner.swift | 15 +++++++++++---- Utilities/IdeogramCaptionGenerator.swift | 2 +- Utilities/MfluxInstaller.swift | 12 ++---------- Utilities/ScenarioGenerator.swift | 2 +- Utilities/UvInstaller.swift | 13 +++++++++++++ Views/Settings/PromptLLMSettingsView.swift | 14 ++++++++------ 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Utilities/GemmaChatRunner.swift b/Utilities/GemmaChatRunner.swift index 16e2d03..e9ba2fc 100644 --- a/Utilities/GemmaChatRunner.swift +++ b/Utilities/GemmaChatRunner.swift @@ -7,7 +7,7 @@ enum GemmaChatRunnerError: LocalizedError { var errorDescription: String? { switch self { case .uvNotFound: - "uv not found at ~/.local/bin/uv. Install from https://docs.astral.sh/uv/" + "uv not found. Install from https://docs.astral.sh/uv/ (or: brew install uv)." case let .modelNotFound(path): "Gemma model not found at \(path). Check the model path in Settings → Advanced " + "(it powers captions and the Scenario Generator)." @@ -22,7 +22,8 @@ enum GemmaChatRunnerError: LocalizedError { /// mlx_lm output reply-region extractor, and the Gemma chat-template /// assembler. enum GemmaChatRunner { - static let uvPath = NSHomeDirectory() + "/.local/bin/uv" + /// Empty when uv is installed nowhere; callers guard with `fileExists`. + nonisolated static var uvPath: String { UvInstaller.resolvedPath } /// uv `--with` requirements. Bumping a floor forces uv past its cached /// resolution, so raise these when a model needs a newer architecture. static let mlxLMRequirement = "mlx-lm>=0.31.3" @@ -158,7 +159,10 @@ enum GemmaChatRunner { temp: Double, environment: [String: String] ) async throws -> (output: String, exitCode: Int32) { - guard FileManager.default.fileExists(atPath: uvPath) else { + // Resolve once: `uvPath` probes the filesystem on every read, so the + // guard and the spawns below must share one answer. + let uv = uvPath + guard !uv.isEmpty else { throw GemmaChatRunnerError.uvNotFound } @@ -181,6 +185,7 @@ enum GemmaChatRunner { } let first = try await spawn( + uv: uv, arguments: arguments(command: "mlx_lm.generate", package: mlxLMRequirement, extra: ["--temp", "\(temp)"]), environment: environment ) @@ -191,6 +196,7 @@ enum GemmaChatRunner { // the generated text, so extraction gets clean output. if first.exitCode != 0, first.output.contains("Model type"), first.output.contains("not supported") { return try await spawn( + uv: uv, arguments: arguments( command: "mlx_vlm.generate", package: mlxVLMRequirement, extra: ["--temperature", "\(temp)", "--no-verbose"] @@ -202,11 +208,12 @@ enum GemmaChatRunner { } private static func spawn( + uv: String, arguments: [String], environment: [String: String] ) async throws -> (output: String, exitCode: Int32) { let process = Process() - process.executableURL = URL(fileURLWithPath: uvPath) + process.executableURL = URL(fileURLWithPath: uv) process.arguments = arguments var env = environment diff --git a/Utilities/IdeogramCaptionGenerator.swift b/Utilities/IdeogramCaptionGenerator.swift index 424cb87..527c471 100644 --- a/Utilities/IdeogramCaptionGenerator.swift +++ b/Utilities/IdeogramCaptionGenerator.swift @@ -14,7 +14,7 @@ enum IdeogramCaptionGeneratorError: LocalizedError { case .promptFileNotFound: "ideogram_caption_prompt.md not found in app bundle" case .uvNotFound: - "uv not found at ~/.local/bin/uv. Install from https://docs.astral.sh/uv/" + "uv not found. Install from https://docs.astral.sh/uv/ (or: brew install uv)." case let .subprocessFailed(code, output): // The tail, not the head — Python tracebacks put the actual // exception on the last lines. diff --git a/Utilities/MfluxInstaller.swift b/Utilities/MfluxInstaller.swift index c11cf98..c2fef02 100644 --- a/Utilities/MfluxInstaller.swift +++ b/Utilities/MfluxInstaller.swift @@ -103,16 +103,8 @@ nonisolated enum MfluxInstaller { } private static func resolveUv() -> String? { - if FileManager.default.fileExists(atPath: UvInstaller.installPath.path) { - return UvInstaller.installPath.path - } - let home = NSHomeDirectory() - let candidates = [ - "\(home)/.local/bin/uv", - "/opt/homebrew/bin/uv", - "/usr/local/bin/uv", - ] - return candidates.first { FileManager.default.fileExists(atPath: $0) } + let path = UvInstaller.resolvedPath + return path.isEmpty ? nil : path } private static func installUv() async throws -> String { diff --git a/Utilities/ScenarioGenerator.swift b/Utilities/ScenarioGenerator.swift index 2808ac7..b4fbc1d 100644 --- a/Utilities/ScenarioGenerator.swift +++ b/Utilities/ScenarioGenerator.swift @@ -60,7 +60,7 @@ enum ScenarioGeneratorError: LocalizedError { case .promptFileNotFound: "scenario_prompt.md not found in app bundle" case .uvNotFound: - "uv not found at ~/.local/bin/uv. Install from https://docs.astral.sh/uv/" + "uv not found. Install from https://docs.astral.sh/uv/ (or: brew install uv)." case let .subprocessFailed(code, output): // The tail, not the head — Python tracebacks put the actual // exception on the last lines. diff --git a/Utilities/UvInstaller.swift b/Utilities/UvInstaller.swift index b58cd1b..5898633 100644 --- a/Utilities/UvInstaller.swift +++ b/Utilities/UvInstaller.swift @@ -22,6 +22,19 @@ nonisolated enum UvInstaller { return base.appendingPathComponent("MLXBits Image Studio/bin/uv") } + /// The uv binary every uv-driven feature should run: the app-managed install + /// first, then the standard user and package-manager locations (Homebrew's + /// `/opt/homebrew/bin` among them). Empty when uv is installed nowhere — + /// matching ``BinaryDetector/detect(_:)``, so callers keep guarding with + /// `fileExists`. Computed, not cached: the Settings indicator has to notice + /// a uv installed after launch. + static var resolvedPath: String { + if FileManager.default.fileExists(atPath: installPath.path) { + return installPath.path + } + return BinaryDetector.detect("uv") + } + static func install() async throws -> String { #if arch(arm64) let archName = "aarch64" diff --git a/Views/Settings/PromptLLMSettingsView.swift b/Views/Settings/PromptLLMSettingsView.swift index 126651b..4c310f6 100644 --- a/Views/Settings/PromptLLMSettingsView.swift +++ b/Views/Settings/PromptLLMSettingsView.swift @@ -53,8 +53,8 @@ struct PromptLLMSettingsView: View { @ViewBuilder private var localFields: some View { @Bindable var s = settings - let uvPath = NSHomeDirectory() + "/.local/bin/uv" - let uvFound = FileManager.default.fileExists(atPath: uvPath) + let uvPath = GemmaChatRunner.uvPath + let uvFound = !uvPath.isEmpty VStack(alignment: .leading, spacing: 4) { TextField("mlx-community/gemma-3-12b-it-4bit", text: $s.gemmaModelPath) .textFieldStyle(.roundedBorder) @@ -72,12 +72,14 @@ struct PromptLLMSettingsView: View { .foregroundStyle(uvFound ? Color.green : Color.red) Text( uvFound - ? "uv found — \(GemmaChatRunner.mlxLMRequirement) / \(GemmaChatRunner.mlxVLMRequirement) " - + "managed automatically" - : "uv not found at ~/.local/bin/uv" + ? "uv found at \(uvPath) — \(GemmaChatRunner.mlxLMRequirement) / " + + "\(GemmaChatRunner.mlxVLMRequirement) managed automatically" + : "uv not found — install from https://docs.astral.sh/uv/ (or: brew install uv)" ) .font(.caption).foregroundStyle(.secondary) - .lineLimit(1).truncationMode(.middle) + // Tail, not middle: the resolved path now sits at the head of the + // string and middle truncation would eat exactly that. + .lineLimit(1).truncationMode(.tail) } .padding(.vertical, 2) } From 698fa42b29c712114d8b3608562f7fa94b1ba0aa Mon Sep 17 00:00:00 2001 From: Bobby Vensko Date: Wed, 23 Sep 2026 19:22:57 -0400 Subject: [PATCH 2/3] style: apply swiftformat to the uvPath accessor The build's SwiftFormat pre-build phase expands the one-line computed property added by the uv resolution fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SF8fB465MqsDHCYnKYzEoL --- Utilities/GemmaChatRunner.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Utilities/GemmaChatRunner.swift b/Utilities/GemmaChatRunner.swift index e9ba2fc..b44542b 100644 --- a/Utilities/GemmaChatRunner.swift +++ b/Utilities/GemmaChatRunner.swift @@ -23,7 +23,10 @@ enum GemmaChatRunnerError: LocalizedError { /// assembler. enum GemmaChatRunner { /// Empty when uv is installed nowhere; callers guard with `fileExists`. - nonisolated static var uvPath: String { UvInstaller.resolvedPath } + nonisolated static var uvPath: String { + UvInstaller.resolvedPath + } + /// uv `--with` requirements. Bumping a floor forces uv past its cached /// resolution, so raise these when a model needs a newer architecture. static let mlxLMRequirement = "mlx-lm>=0.31.3" From a7fef856c6ad0d5ebec5e52042ee4ba529e7762a Mon Sep 17 00:00:00 2001 From: plz12345 <132735020+plz12345@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:44:47 -0400 Subject: [PATCH 3/3] Clarify when the Settings uv indicator picks up a new install --- Utilities/UvInstaller.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Utilities/UvInstaller.swift b/Utilities/UvInstaller.swift index 5898633..fc68a15 100644 --- a/Utilities/UvInstaller.swift +++ b/Utilities/UvInstaller.swift @@ -26,8 +26,9 @@ nonisolated enum UvInstaller { /// first, then the standard user and package-manager locations (Homebrew's /// `/opt/homebrew/bin` among them). Empty when uv is installed nowhere — /// matching ``BinaryDetector/detect(_:)``, so callers keep guarding with - /// `fileExists`. Computed, not cached: the Settings indicator has to notice - /// a uv installed after launch. + /// `fileExists`. Computed, not cached, so each read sees the current disk: + /// a uv installed after launch shows up in the Settings indicator the next + /// time that view redraws — nothing here triggers the redraw. static var resolvedPath: String { if FileManager.default.fileExists(atPath: installPath.path) { return installPath.path