mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
fix(kanban): show stderr-only in error banner, parse stdout-only as JSON
`KanbanViewModel.load` previously assigned the combined stdout+stderr output of `runHermesCLI` into both the JSON-parse `data` and the `stderr` slot of its result tuple. Two consequences: - On non-zero exit, the error banner showed combined output (often stdout usage text concatenated with the actual error), reducing the signal-to-noise ratio when troubleshooting. - On non-zero exit with mixed output, JSON decoding could fail because stderr text was prepended to the JSON body. Added `HermesFileService.runHermesCLISplit` — a sibling of `runHermesCLI` that returns `(exitCode, stdout, stderr)` separately, leaning on the already-separated `stdoutString` / `stderrString` from the transport layer. KanbanViewModel now uses it: stdout is the JSON parse target, stderr is the error-banner source. Existing `runHermesCLI` callers are untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1577,6 +1577,39 @@ struct HermesFileService: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Split-stream variant of `runHermesCLI`. Use this when you need to
|
||||
/// parse stdout (e.g. JSON output) without stderr contamination, and
|
||||
/// surface stderr separately as a user-facing error message. Transport
|
||||
/// failures land in `stderr` with an empty `stdout`.
|
||||
@discardableResult
|
||||
nonisolated func runHermesCLISplit(args: [String], timeout: TimeInterval = 60, stdinInput: String? = nil) -> (exitCode: Int32, stdout: String, stderr: String) {
|
||||
let binary: String
|
||||
if context.isRemote {
|
||||
binary = context.paths.hermesBinary
|
||||
} else {
|
||||
guard let local = hermesBinaryPath() else { return (-1, "", "hermes binary not found") }
|
||||
binary = local
|
||||
}
|
||||
|
||||
let stdinData = stdinInput?.data(using: .utf8)
|
||||
do {
|
||||
let result = try transport.runProcess(
|
||||
executable: binary,
|
||||
args: args,
|
||||
stdin: stdinData,
|
||||
timeout: timeout
|
||||
)
|
||||
return (result.exitCode, result.stdoutString, result.stderrString)
|
||||
} catch let error as TransportError {
|
||||
let message = error.diagnosticStderr.isEmpty
|
||||
? (error.errorDescription ?? "transport error")
|
||||
: error.diagnosticStderr
|
||||
return (-1, "", message)
|
||||
} catch {
|
||||
return (-1, "", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File I/O
|
||||
|
||||
/// Read a UTF-8 text file through the transport. Missing files and any
|
||||
|
||||
@@ -71,18 +71,17 @@ final class KanbanViewModel {
|
||||
isLoading = true
|
||||
let svc = fileService
|
||||
let filter = statusFilter
|
||||
let result = await Task.detached { () -> (data: Data?, exitCode: Int32, stderr: String) in
|
||||
let result = await Task.detached { () -> (exitCode: Int32, stdout: String, stderr: String) in
|
||||
var args = ["kanban", "list", "--json"]
|
||||
if filter != .all {
|
||||
args.append(contentsOf: ["--status", filter.rawValue])
|
||||
}
|
||||
let r = svc.runHermesCLI(args: args, timeout: 15)
|
||||
return (r.output.data(using: .utf8), r.exitCode, r.output)
|
||||
return svc.runHermesCLISplit(args: args, timeout: 15)
|
||||
}.value
|
||||
|
||||
defer { isLoading = false }
|
||||
|
||||
guard result.exitCode == 0, let data = result.data else {
|
||||
guard result.exitCode == 0 else {
|
||||
lastError = result.stderr.isEmpty
|
||||
? "kanban list failed (\(result.exitCode))"
|
||||
: result.stderr
|
||||
@@ -90,6 +89,12 @@ final class KanbanViewModel {
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = result.stdout.data(using: .utf8) else {
|
||||
lastError = "kanban list returned non-UTF8 output"
|
||||
tasks = []
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let decoded = try JSONDecoder().decode([HermesKanbanTask].self, from: data)
|
||||
tasks = decoded
|
||||
@@ -98,7 +103,7 @@ final class KanbanViewModel {
|
||||
// Hermes may print a "no matching tasks" line as text instead of
|
||||
// empty JSON; handle gracefully so the UI shows an empty list
|
||||
// without raising an error banner.
|
||||
if String(data: data, encoding: .utf8)?.contains("no matching tasks") == true {
|
||||
if result.stdout.contains("no matching tasks") {
|
||||
tasks = []
|
||||
lastError = nil
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user