From 99f734bf0b6fdee3e46a49403e952c6e19160bd1 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Sat, 25 Apr 2026 09:36:57 +0200 Subject: [PATCH] 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) --- scarf/Scarf iOS/Memory/MemoryListView.swift | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/scarf/Scarf iOS/Memory/MemoryListView.swift b/scarf/Scarf iOS/Memory/MemoryListView.swift index 02572fd..4889729 100644 --- a/scarf/Scarf iOS/Memory/MemoryListView.swift +++ b/scarf/Scarf iOS/Memory/MemoryListView.swift @@ -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