mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +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
|
// MARK: - File I/O
|
||||||
|
|
||||||
/// Read a UTF-8 text file through the transport. Missing files and any
|
/// Read a UTF-8 text file through the transport. Missing files and any
|
||||||
|
|||||||
@@ -71,18 +71,17 @@ final class KanbanViewModel {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
let svc = fileService
|
let svc = fileService
|
||||||
let filter = statusFilter
|
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"]
|
var args = ["kanban", "list", "--json"]
|
||||||
if filter != .all {
|
if filter != .all {
|
||||||
args.append(contentsOf: ["--status", filter.rawValue])
|
args.append(contentsOf: ["--status", filter.rawValue])
|
||||||
}
|
}
|
||||||
let r = svc.runHermesCLI(args: args, timeout: 15)
|
return svc.runHermesCLISplit(args: args, timeout: 15)
|
||||||
return (r.output.data(using: .utf8), r.exitCode, r.output)
|
|
||||||
}.value
|
}.value
|
||||||
|
|
||||||
defer { isLoading = false }
|
defer { isLoading = false }
|
||||||
|
|
||||||
guard result.exitCode == 0, let data = result.data else {
|
guard result.exitCode == 0 else {
|
||||||
lastError = result.stderr.isEmpty
|
lastError = result.stderr.isEmpty
|
||||||
? "kanban list failed (\(result.exitCode))"
|
? "kanban list failed (\(result.exitCode))"
|
||||||
: result.stderr
|
: result.stderr
|
||||||
@@ -90,6 +89,12 @@ final class KanbanViewModel {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let data = result.stdout.data(using: .utf8) else {
|
||||||
|
lastError = "kanban list returned non-UTF8 output"
|
||||||
|
tasks = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let decoded = try JSONDecoder().decode([HermesKanbanTask].self, from: data)
|
let decoded = try JSONDecoder().decode([HermesKanbanTask].self, from: data)
|
||||||
tasks = decoded
|
tasks = decoded
|
||||||
@@ -98,7 +103,7 @@ final class KanbanViewModel {
|
|||||||
// Hermes may print a "no matching tasks" line as text instead of
|
// Hermes may print a "no matching tasks" line as text instead of
|
||||||
// empty JSON; handle gracefully so the UI shows an empty list
|
// empty JSON; handle gracefully so the UI shows an empty list
|
||||||
// without raising an error banner.
|
// without raising an error banner.
|
||||||
if String(data: data, encoding: .utf8)?.contains("no matching tasks") == true {
|
if result.stdout.contains("no matching tasks") {
|
||||||
tasks = []
|
tasks = []
|
||||||
lastError = nil
|
lastError = nil
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user