From 1eb5c92f6abbeafd21d8b87f94eae572545c2f4e Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Tue, 5 May 2026 13:12:55 +0200 Subject: [PATCH] fix(aux-tab): correct nested-YAML parser so unknown-task surface works on remote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Settings/Views/Tabs/AuxiliaryTab.swift | 108 +++++++++--------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/scarf/scarf/Features/Settings/Views/Tabs/AuxiliaryTab.swift b/scarf/scarf/Features/Settings/Views/Tabs/AuxiliaryTab.swift index 3d46f67..9a001bb 100644 --- a/scarf/scarf/Features/Settings/Views/Tabs/AuxiliaryTab.swift +++ b/scarf/scarf/Features/Settings/Views/Tabs/AuxiliaryTab.swift @@ -61,65 +61,71 @@ struct AuxiliaryTab: View { /// fall-through "Other tasks in config.yaml" section. private var unknownTasks: [String] { 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 { var found: Set = [] - // Scan top-level `auxiliary..` 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.` 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 `auxiliary:` then collect - // direct child keys. - let lines = viewModel.rawConfigYAML.components(separatedBy: "\n") var inAuxBlock = false 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 indent = line.count - trimmed.count - if trimmed.hasPrefix("auxiliary:") { - inAuxBlock = true - auxIndent = indent - continue - } - guard inAuxBlock else { continue } - // Out of the aux block when indent drops back to or - // below auxIndent on a non-empty line. - if !trimmed.isEmpty && indent <= auxIndent { - inAuxBlock = false - continue - } - // Direct children of `auxiliary:` are at indent - // > auxIndent and contain a `key:` (no further nesting - // dots before the colon). - if indent > auxIndent, - let colonIdx = trimmed.firstIndex(of: ":") { - let key = trimmed[.. taskIndent. + guard indent == taskIndent else { continue } + // The line should look like `:` or `: `. + // Match `:` at the start to filter out + // things like flow-style maps `[a, b]:` that aren't + // task definitions. + guard let colonIdx = trimmed.firstIndex(of: ":") else { continue } + let key = trimmed[..