feat(ios-memory): hermes memory reset on iOS too (cross-platform parity)

Mac shipped the toolbar Reset button in Phase 5; iOS gets it in the
final verification pass for parity.

iOS MemoryListView:
- Toolbar button (counterclockwise icon) opens a destructive
  confirmation dialog matching the Mac copy.
- resetMemory() shells out via context.makeTransport().runProcess,
  using the same PATH-prefix trick IOSSettingsViewModel.saveValue
  uses so non-interactive remote shells find hermes in ~/.local/bin
  / /opt/homebrew/bin / ~/.hermes/bin.
- Success and failure both surface alerts (success message
  confirms the wipe; failure surfaces stderr+stdout combined).

Verified: iOS build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-25 09:36:57 +02:00
parent ca1eb54a5b
commit 99f734bf0b
@@ -9,6 +9,9 @@ import ScarfCore
/// `IOSMemoryViewModel` which lives in ScarfCore.
struct MemoryListView: View {
let config: IOSServerConfig
@State private var showResetConfirm = false
@State private var resetError: String?
@State private var resetSucceeded = false
private static let sharedContextID: ServerID = ServerID(
uuidString: "00000000-0000-0000-0000-0000000000A1"
@@ -32,6 +35,77 @@ struct MemoryListView: View {
.scarfGoListDensity()
.navigationTitle("Memory")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
// v2.5: `hermes memory reset` (Hermes v2026.4.23+) wipes
// both MEMORY.md and USER.md atomically. Surfaced as a
// toolbar button (smaller fat-finger target than a list
// row) gated behind a destructive confirmation dialog.
ToolbarItem(placement: .topBarTrailing) {
Button {
showResetConfirm = true
} label: {
Image(systemName: "arrow.counterclockwise")
}
.accessibilityLabel("Reset memory")
}
}
.confirmationDialog(
"Reset memory?",
isPresented: $showResetConfirm,
titleVisibility: .visible
) {
Button("Reset", role: .destructive) {
Task { await resetMemory(context: ctx) }
}
Button("Cancel", role: .cancel) {}
} message: {
Text("Wipes MEMORY.md and USER.md to empty via `hermes memory reset --yes`. The agent's accumulated knowledge for this server is gone immediately. Use this only when a session went off the rails.")
}
.alert("Couldn't reset memory", isPresented: Binding(
get: { resetError != nil },
set: { if !$0 { resetError = nil } }
)) {
Button("OK") { resetError = nil }
} message: {
Text(resetError ?? "")
}
.alert("Memory reset", isPresented: $resetSucceeded) {
Button("OK") {}
} message: {
Text("MEMORY.md and USER.md were cleared on the host.")
}
}
/// Run `hermes memory reset --yes` over the iOS context's transport
/// (Citadel SSH exec). Mirrors the PATH-prefix trick
/// IOSSettingsViewModel.saveValue uses so non-interactive shells
/// find hermes even when it's in `~/.local/bin` or `/opt/homebrew/bin`.
private func resetMemory(context: ServerContext) async {
let hermes = context.paths.hermesBinary
let script = "PATH=\"$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$HOME/.hermes/bin:$PATH\" \(hermes) memory reset --yes"
let ctx = context
do {
let result = try await Task.detached {
try ctx.makeTransport().runProcess(
executable: "/bin/sh",
args: ["-c", script],
stdin: nil,
timeout: 15
)
}.value
if result.exitCode == 0 {
resetSucceeded = true
} else {
let stderr = result.stderrString.trimmingCharacters(in: .whitespacesAndNewlines)
let stdout = result.stdoutString.trimmingCharacters(in: .whitespacesAndNewlines)
let combined = [stderr, stdout].filter { !$0.isEmpty }.joined(separator: "\n")
resetError = combined.isEmpty
? "hermes memory reset exited with status \(result.exitCode)."
: combined
}
} catch {
resetError = "Couldn't reach Hermes: \(error.localizedDescription)"
}
}
@ViewBuilder