mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
M9 #4.3: scoped Settings editor via hermes config set
Pass-1 feedback: "Settings loads, but no fields are editable." By- design read-only in M6, but the on-the-go story is weaker without at least the core model / approval-mode / display toggles editable. Not a generic YAML round-trip editor — that was ruled out in the original iOS plan because comment/order preservation requires Hermes-side changes or a significant YAML library. Instead: - Curated v1 list of 7 editable keys: model.default, model.provider, approvals.mode, agent.max_turns, display.show_cost / show_reasoning / streaming. Covers ~80% of actual "I want to change this right now while I'm away from my Mac" scenarios. - IOSSettingsViewModel.saveValue(key:value:) shells out to `hermes config set <key> <value>` over the SSH transport's runProcess, reusing the same PATH-prefix trick we added in pass-1 for hermes acp so the remote shell finds hermes even in non- interactive mode. Hermes owns the YAML round-trip; Scarf just picks the value. - SettingEditorSheet renders the right control per key: Toggle (booleans), segmented Picker (approval mode), Stepper (max_turns), TextField (model / provider / timezone). One sheet, four kinds of input, driven by a `SettingSpec.Kind` enum. - SettingsView gets a "Quick edits" section at the top that lists the 7 keys with their current parsed values + an edit affordance. The existing 10+ read-only sections stay unchanged — editing stays scoped to the keys we curated. - On save, the VM calls `load()` again so the parsed config (and therefore the Quick-edits labels + the read-only sections below) reflects the new value immediately. - Errors from `hermes config set` (non-zero exit) surface inline on the sheet via SettingsSaveError.commandFailed.errorDescription, carrying stderr/stdout combined so the user sees what the remote complained about. Sheet stays open on error for retry. ScarfGo builds green. Mac Settings is unaffected — this feature is iOS-only (Mac has its own richer editors via HermesFileService). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,4 +55,76 @@ public final class IOSSettingsViewModel {
|
||||
config = HermesConfig(yaml: text)
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
/// Set a dotted config key on the remote via `hermes config set`.
|
||||
/// Hermes owns the YAML round-trip (preserves comments, key
|
||||
/// order, formatting); Scarf just picks the value. Reloads the
|
||||
/// parsed config on success so the UI reflects the change
|
||||
/// immediately.
|
||||
///
|
||||
/// Pass-1 M9 #4.3 — lets on-the-go users flip `model.default`,
|
||||
/// `agent.approval_mode`, `display.show_cost` etc. without going
|
||||
/// back to the Mac app. Scope intentionally narrow: a curated
|
||||
/// list of keys in the editor sheet, not a generic YAML writer.
|
||||
///
|
||||
/// Throws on non-zero exit or connection failure. Callers should
|
||||
/// surface the error to the user (usually a banner on the editor
|
||||
/// sheet) and leave the sheet open for retry.
|
||||
public func saveValue(key: String, value: String) async throws {
|
||||
isSaving = true
|
||||
defer { isSaving = false }
|
||||
|
||||
let ctx = context
|
||||
let hermes = ctx.paths.hermesBinary
|
||||
// Pass through the same PATH-prefix trick ACPClient+iOS uses
|
||||
// (pass-1 M7 #5) so remote non-interactive shells find hermes
|
||||
// even when it's in ~/.local/bin or /opt/homebrew/bin.
|
||||
let script = "PATH=\"$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$HOME/.hermes/bin:$PATH\" \(hermes) config set \(shellEscape(key)) \(shellEscape(value))"
|
||||
|
||||
let result: ProcessResult = try await Task.detached {
|
||||
try ctx.makeTransport().runProcess(
|
||||
executable: "/bin/sh",
|
||||
args: ["-c", script],
|
||||
stdin: nil,
|
||||
timeout: 15
|
||||
)
|
||||
}.value
|
||||
|
||||
if result.exitCode != 0 {
|
||||
let stderr = result.stderrString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let stdout = result.stdoutString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let combined = [stderr, stdout].filter { !$0.isEmpty }.joined(separator: "\n")
|
||||
throw SettingsSaveError.commandFailed(
|
||||
exitCode: result.exitCode,
|
||||
message: combined.isEmpty ? "hermes config set exited with code \(result.exitCode)" : combined
|
||||
)
|
||||
}
|
||||
|
||||
// Reload so the UI reflects the just-written value.
|
||||
await load()
|
||||
}
|
||||
|
||||
/// True while a `saveValue(...)` call is in flight. Sheet uses
|
||||
/// this to disable the Save button + show a ProgressView.
|
||||
public private(set) var isSaving: Bool = false
|
||||
|
||||
/// Single-quote-escape a shell argument. Handles embedded single
|
||||
/// quotes via the standard `'"'"'` trick. Used to quote both the
|
||||
/// key and the value on the remote command line.
|
||||
private func shellEscape(_ s: String) -> String {
|
||||
"'" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors surfaced by `IOSSettingsViewModel.saveValue`. Kept public
|
||||
/// so SettingEditorSheet (ScarfGo) can narrow on commandFailed to
|
||||
/// show the stderr payload inline instead of just the generic text.
|
||||
public enum SettingsSaveError: Error, LocalizedError {
|
||||
case commandFailed(exitCode: Int32, message: String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .commandFailed(_, let message): return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user