mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
fix: Move Tools subprocess calls off main thread to fix toggle rendering
Synchronous Process.run()/waitUntilExit() calls on the main thread blocked SwiftUI's render loop, causing toggle controls to appear as solid blue rectangles instead of proper switches. All hermes subprocess and file I/O calls are now async via Task.detached, toggle uses optimistic state update for immediate visual feedback, and pipe file handles are properly closed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,40 +1,56 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import os
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
final class ToolsViewModel {
|
final class ToolsViewModel {
|
||||||
|
private let logger = Logger(subsystem: "com.scarf", category: "ToolsViewModel")
|
||||||
|
|
||||||
var selectedPlatform: HermesToolPlatform = KnownPlatforms.cli
|
var selectedPlatform: HermesToolPlatform = KnownPlatforms.cli
|
||||||
var toolsets: [HermesToolset] = []
|
var toolsets: [HermesToolset] = []
|
||||||
var mcpStatus: String = ""
|
var mcpStatus: String = ""
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
var availablePlatforms: [HermesToolPlatform] = []
|
var availablePlatforms: [HermesToolPlatform] = []
|
||||||
|
|
||||||
func load() {
|
@MainActor
|
||||||
loadPlatforms()
|
func load() async {
|
||||||
loadTools(for: selectedPlatform)
|
isLoading = true
|
||||||
loadMCPStatus()
|
await loadPlatforms()
|
||||||
|
await loadTools(for: selectedPlatform)
|
||||||
|
await loadMCPStatus()
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchPlatform(_ platform: HermesToolPlatform) {
|
@MainActor
|
||||||
|
func switchPlatform(_ platform: HermesToolPlatform) async {
|
||||||
selectedPlatform = platform
|
selectedPlatform = platform
|
||||||
loadTools(for: platform)
|
await loadTools(for: platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleTool(_ tool: HermesToolset) {
|
@MainActor
|
||||||
let action = tool.enabled ? "disable" : "enable"
|
func toggleTool(_ tool: HermesToolset) async {
|
||||||
let result = runHermes(["tools", action, tool.name, "--platform", selectedPlatform.name])
|
guard let idx = toolsets.firstIndex(where: { $0.name == tool.name }) else { return }
|
||||||
if result.exitCode == 0 {
|
|
||||||
if let idx = toolsets.firstIndex(where: { $0.name == tool.name }) {
|
|
||||||
toolsets[idx].enabled.toggle()
|
toolsets[idx].enabled.toggle()
|
||||||
|
let newEnabled = toolsets[idx].enabled
|
||||||
|
|
||||||
|
let action = newEnabled ? "enable" : "disable"
|
||||||
|
let result = await runHermes(["tools", action, tool.name, "--platform", selectedPlatform.name])
|
||||||
|
|
||||||
|
if result.exitCode != 0 {
|
||||||
|
if let idx = toolsets.firstIndex(where: { $0.name == tool.name }) {
|
||||||
|
toolsets[idx].enabled = !newEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadPlatforms() {
|
@MainActor
|
||||||
|
private func loadPlatforms() async {
|
||||||
let config: String
|
let config: String
|
||||||
do {
|
do {
|
||||||
config = try String(contentsOfFile: HermesPaths.configYAML, encoding: .utf8)
|
config = try await Task.detached {
|
||||||
|
try String(contentsOfFile: HermesPaths.configYAML, encoding: .utf8)
|
||||||
|
}.value
|
||||||
} catch {
|
} catch {
|
||||||
print("[Scarf] Failed to read config.yaml: \(error.localizedDescription)")
|
logger.error("Failed to read config.yaml: \(error.localizedDescription)")
|
||||||
config = ""
|
config = ""
|
||||||
}
|
}
|
||||||
var platforms: [HermesToolPlatform] = []
|
var platforms: [HermesToolPlatform] = []
|
||||||
@@ -67,15 +83,15 @@ final class ToolsViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadTools(for platform: HermesToolPlatform) {
|
@MainActor
|
||||||
isLoading = true
|
private func loadTools(for platform: HermesToolPlatform) async {
|
||||||
let result = runHermes(["tools", "list", "--platform", platform.name])
|
let result = await runHermes(["tools", "list", "--platform", platform.name])
|
||||||
toolsets = parseToolsList(result.output)
|
toolsets = parseToolsList(result.output)
|
||||||
isLoading = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadMCPStatus() {
|
@MainActor
|
||||||
let result = runHermes(["mcp", "list"])
|
private func loadMCPStatus() async {
|
||||||
|
let result = await runHermes(["mcp", "list"])
|
||||||
mcpStatus = result.output.trimmingCharacters(in: .whitespacesAndNewlines)
|
mcpStatus = result.output.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,21 +137,32 @@ final class ToolsViewModel {
|
|||||||
return "🔧"
|
return "🔧"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func runHermes(_ arguments: [String]) -> (output: String, exitCode: Int32) {
|
private nonisolated func runHermes(_ arguments: [String]) async -> (output: String, exitCode: Int32) {
|
||||||
|
await Task.detached {
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.executableURL = URL(fileURLWithPath: HermesPaths.hermesBinary)
|
process.executableURL = URL(fileURLWithPath: HermesPaths.hermesBinary)
|
||||||
process.arguments = arguments
|
process.arguments = arguments
|
||||||
let pipe = Pipe()
|
let stdoutPipe = Pipe()
|
||||||
process.standardOutput = pipe
|
let stderrPipe = Pipe()
|
||||||
process.standardError = Pipe()
|
process.standardOutput = stdoutPipe
|
||||||
|
process.standardError = stderrPipe
|
||||||
do {
|
do {
|
||||||
try process.run()
|
try process.run()
|
||||||
process.waitUntilExit()
|
process.waitUntilExit()
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
let output = String(data: data, encoding: .utf8) ?? ""
|
let output = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
try? stdoutPipe.fileHandleForReading.close()
|
||||||
|
try? stdoutPipe.fileHandleForWriting.close()
|
||||||
|
try? stderrPipe.fileHandleForReading.close()
|
||||||
|
try? stderrPipe.fileHandleForWriting.close()
|
||||||
return (output, process.terminationStatus)
|
return (output, process.terminationStatus)
|
||||||
} catch {
|
} catch {
|
||||||
|
try? stdoutPipe.fileHandleForReading.close()
|
||||||
|
try? stdoutPipe.fileHandleForWriting.close()
|
||||||
|
try? stderrPipe.fileHandleForReading.close()
|
||||||
|
try? stderrPipe.fileHandleForWriting.close()
|
||||||
return ("", -1)
|
return ("", -1)
|
||||||
}
|
}
|
||||||
|
}.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct ToolsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Tools")
|
.navigationTitle("Tools")
|
||||||
.onAppear { viewModel.load() }
|
.task { await viewModel.load() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var platformPicker: some View {
|
private var platformPicker: some View {
|
||||||
@@ -23,7 +23,7 @@ struct ToolsView: View {
|
|||||||
get: { viewModel.selectedPlatform.name },
|
get: { viewModel.selectedPlatform.name },
|
||||||
set: { name in
|
set: { name in
|
||||||
if let platform = viewModel.availablePlatforms.first(where: { $0.name == name }) {
|
if let platform = viewModel.availablePlatforms.first(where: { $0.name == name }) {
|
||||||
viewModel.switchPlatform(platform)
|
Task { await viewModel.switchPlatform(platform) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)) {
|
)) {
|
||||||
@@ -46,7 +46,7 @@ struct ToolsView: View {
|
|||||||
LazyVStack(spacing: 1) {
|
LazyVStack(spacing: 1) {
|
||||||
ForEach(viewModel.toolsets) { tool in
|
ForEach(viewModel.toolsets) { tool in
|
||||||
ToolRow(tool: tool) {
|
ToolRow(tool: tool) {
|
||||||
viewModel.toggleTool(tool)
|
await viewModel.toggleTool(tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ struct ToolsView: View {
|
|||||||
|
|
||||||
struct ToolRow: View {
|
struct ToolRow: View {
|
||||||
let tool: HermesToolset
|
let tool: HermesToolset
|
||||||
let onToggle: () -> Void
|
let onToggle: () async -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
@@ -95,7 +95,7 @@ struct ToolRow: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
Toggle("", isOn: Binding(
|
Toggle("", isOn: Binding(
|
||||||
get: { tool.enabled },
|
get: { tool.enabled },
|
||||||
set: { _ in onToggle() }
|
set: { _ in Task { await onToggle() } }
|
||||||
))
|
))
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
|
|||||||
Reference in New Issue
Block a user