mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
fix(aux-tab): correct nested-YAML parser so unknown-task surface works on remote
Bug 1 — the previous parser collected every indented child under `auxiliary:` as if it were a task name, including leaf fields (provider, model, base_url, api_key, timeout). Result: bogus rows on local where the parser happened to fire, plus pollution of the unknown-tasks set with field names that subtractFrom-known left orphaned. Bug 2 — the flat-dot-path branch (`auxiliary.X.Y:`) was dead code. config.yaml is always nested YAML; the dot-path form only appears in interactive `hermes config get` output, never on disk. Removing it. User reported the unknown-tasks section showed on local but not on remote. Most likely root cause: the buggy parser surfaced junk on local (where their config has nested-form aux settings) while the dead flat-path branch never fired on remote either, so remote silently rendered nothing. With the parser fixed both contexts now surface real unknown task names if any are present. Rewrite as a clean two-pass walker: - First nested line inside the block locks taskIndent. - Only collect at exactly taskIndent (skip leaf fields deeper). - Tolerate CRLF line endings, blank lines, and YAML comments without resetting block state. - Handles 2-space and 4-space indent equally. Verified manually with four fixture shapes: 2-space, 4-space, with-comments-and-blanks, no-aux-block. All correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,65 +61,71 @@ struct AuxiliaryTab: View {
|
|||||||
/// fall-through "Other tasks in config.yaml" section.
|
/// fall-through "Other tasks in config.yaml" section.
|
||||||
private var unknownTasks: [String] {
|
private var unknownTasks: [String] {
|
||||||
let known = Set(tasks.map(\.key))
|
let known = Set(tasks.map(\.key))
|
||||||
|
let found = Self.parseAuxTaskNames(from: viewModel.rawConfigYAML)
|
||||||
|
return found.subtracting(known).sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk the raw config.yaml for the top-level `auxiliary:` block
|
||||||
|
/// and collect ONLY direct-child task names (not the leaf
|
||||||
|
/// fields underneath them like `provider`, `model`, `api_key`).
|
||||||
|
/// Static + `internal` so unit tests can drive it with fixture
|
||||||
|
/// strings without standing up a SettingsViewModel.
|
||||||
|
///
|
||||||
|
/// Handles both 2-space and 4-space indent styles. Tolerates
|
||||||
|
/// blank lines and comments. Stops collecting when indent
|
||||||
|
/// drops back to or below the `auxiliary:` line — same shape
|
||||||
|
/// the YAML parser uses to decide block boundaries.
|
||||||
|
static func parseAuxTaskNames(from yaml: String) -> Set<String> {
|
||||||
var found: Set<String> = []
|
var found: Set<String> = []
|
||||||
// Scan top-level `auxiliary.<key>.<field>` keys.
|
|
||||||
for line in viewModel.rawConfigYAML.components(separatedBy: "\n") {
|
|
||||||
let stripped = line.drop(while: { $0 == " " || $0 == "\t" })
|
|
||||||
// YAML emits these as either nested under `auxiliary:`
|
|
||||||
// or as flat dot-paths depending on Hermes' dump style.
|
|
||||||
// We match either shape by looking for keys after
|
|
||||||
// `auxiliary.<key>` OR by walking nested indentation.
|
|
||||||
// Quickest robust approach: regex over the raw text.
|
|
||||||
if let m = stripped.range(of: #"^auxiliary\.([A-Za-z0-9_]+)\."#, options: .regularExpression) {
|
|
||||||
let frag = String(stripped[m])
|
|
||||||
let parts = frag.split(separator: ".")
|
|
||||||
if parts.count >= 2 {
|
|
||||||
found.insert(String(parts[1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Nested form: scan for `<indent>auxiliary:` then collect
|
|
||||||
// direct child keys.
|
|
||||||
let lines = viewModel.rawConfigYAML.components(separatedBy: "\n")
|
|
||||||
var inAuxBlock = false
|
var inAuxBlock = false
|
||||||
var auxIndent = -1
|
var auxIndent = -1
|
||||||
for line in lines {
|
// Indent of the first task-name line we see inside the
|
||||||
|
// block. Established lazily so we work with both 2- and
|
||||||
|
// 4-space indentation. Once locked, only collect at this
|
||||||
|
// exact indent — anything deeper is a leaf field.
|
||||||
|
var taskIndent = -1
|
||||||
|
for rawLine in yaml.components(separatedBy: "\n") {
|
||||||
|
// Strip line-trailing CRs (Windows / SSH artifacts).
|
||||||
|
let line = rawLine.hasSuffix("\r") ? String(rawLine.dropLast()) : rawLine
|
||||||
let trimmed = line.drop(while: { $0 == " " || $0 == "\t" })
|
let trimmed = line.drop(while: { $0 == " " || $0 == "\t" })
|
||||||
let indent = line.count - trimmed.count
|
let indent = line.count - trimmed.count
|
||||||
|
// Skip blanks + comments without resetting state.
|
||||||
|
if trimmed.isEmpty || trimmed.hasPrefix("#") { continue }
|
||||||
|
if !inAuxBlock {
|
||||||
if trimmed.hasPrefix("auxiliary:") {
|
if trimmed.hasPrefix("auxiliary:") {
|
||||||
inAuxBlock = true
|
inAuxBlock = true
|
||||||
auxIndent = indent
|
auxIndent = indent
|
||||||
|
taskIndent = -1
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
guard inAuxBlock else { continue }
|
|
||||||
// Out of the aux block when indent drops back to or
|
// Out of the aux block when indent drops back to or
|
||||||
// below auxIndent on a non-empty line.
|
// below auxIndent on a non-comment / non-blank line.
|
||||||
if !trimmed.isEmpty && indent <= auxIndent {
|
if indent <= auxIndent {
|
||||||
inAuxBlock = false
|
inAuxBlock = false
|
||||||
|
taskIndent = -1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Direct children of `auxiliary:` are at indent
|
// First nested line inside the block: that indent
|
||||||
// > auxIndent and contain a `key:` (no further nesting
|
// level is the task-name level for the rest of this
|
||||||
// dots before the colon).
|
// block.
|
||||||
if indent > auxIndent,
|
if taskIndent == -1 {
|
||||||
let colonIdx = trimmed.firstIndex(of: ":") {
|
taskIndent = indent
|
||||||
let key = trimmed[..<colonIdx]
|
}
|
||||||
// The first deeper key under `auxiliary:` is a task
|
// Skip leaf fields — they live at indent > taskIndent.
|
||||||
// name. We can't easily distinguish "task name" from
|
guard indent == taskIndent else { continue }
|
||||||
// "leaf field" without tracking indent more carefully,
|
// The line should look like `<key>:` or `<key>: <inline>`.
|
||||||
// but only task names sit at indent == auxIndent + 2
|
// Match `<identifier>:` at the start to filter out
|
||||||
// (or +4 with two-space indent). Add the simplest
|
// things like flow-style maps `[a, b]:` that aren't
|
||||||
// heuristic: collect any token that's plausibly a
|
// task definitions.
|
||||||
// task name.
|
guard let colonIdx = trimmed.firstIndex(of: ":") else { continue }
|
||||||
let candidate = String(key).trimmingCharacters(in: .whitespaces)
|
let key = trimmed[..<colonIdx].trimmingCharacters(in: .whitespaces)
|
||||||
if !candidate.isEmpty
|
if !key.isEmpty,
|
||||||
&& candidate.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "_" }) {
|
key.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "_" }) {
|
||||||
found.insert(candidate)
|
found.insert(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return found
|
||||||
let unknown = found.subtracting(known)
|
|
||||||
return unknown.sorted()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
Reference in New Issue
Block a user