diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dc9e4d8..76721e5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -447,7 +447,7 @@ dependencies = [ [[package]] name = "clawpal" -version = "0.1.2" +version = "0.1.3" dependencies = [ "async-trait", "chrono", diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index d507e10..a59d9a8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -4883,7 +4883,7 @@ pub async fn remote_list_history( "id": entry.name, "createdAt": created_at_iso, "source": source, - "canRollback": false, + "canRollback": true, })); } // Sort newest first @@ -4895,6 +4895,60 @@ pub async fn remote_list_history( Ok(serde_json::json!({ "items": items })) } +#[tauri::command] +pub async fn remote_preview_rollback( + pool: State<'_, SshConnectionPool>, + host_id: String, + snapshot_id: String, +) -> Result { + let snapshot_path = format!("~/.clawpal/snapshots/{snapshot_id}"); + let snapshot_text = pool.sftp_read(&host_id, &snapshot_path).await?; + let target: Value = serde_json::from_str(&snapshot_text) + .map_err(|e| format!("Failed to parse snapshot: {e}"))?; + + let current_text = pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await?; + let current: Value = serde_json::from_str(¤t_text) + .map_err(|e| format!("Failed to parse config: {e}"))?; + + let before = serde_json::to_string_pretty(¤t).unwrap_or_else(|_| "{}".into()); + let after = serde_json::to_string_pretty(&target).unwrap_or_else(|_| "{}".into()); + Ok(PreviewResult { + recipe_id: "rollback".into(), + diff: format_diff(¤t, &target), + config_before: before, + config_after: after, + changes: collect_change_paths(¤t, &target), + overwrites_existing: true, + can_rollback: true, + impact_level: "medium".into(), + warnings: vec!["Rollback will replace current configuration".into()], + }) +} + +#[tauri::command] +pub async fn remote_rollback( + pool: State<'_, SshConnectionPool>, + host_id: String, + snapshot_id: String, +) -> Result { + let snapshot_path = format!("~/.clawpal/snapshots/{snapshot_id}"); + let target_text = pool.sftp_read(&host_id, &snapshot_path).await?; + let target: Value = serde_json::from_str(&target_text) + .map_err(|e| format!("Failed to parse snapshot: {e}"))?; + + let current_text = pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await?; + remote_write_config_with_snapshot(&pool, &host_id, ¤t_text, &target, "rollback").await?; + + Ok(ApplyResult { + ok: true, + snapshot_id: Some(snapshot_id), + config_path: "~/.openclaw/openclaw.json".into(), + backup_path: None, + warnings: vec!["rolled back".into()], + errors: Vec::new(), + }) +} + #[tauri::command] pub async fn remote_list_discord_guild_channels( pool: State<'_, SshConnectionPool>, @@ -5017,29 +5071,34 @@ pub async fn remote_analyze_sessions( // Run a shell script via SSH that scans session files and outputs JSON. // This is MUCH faster than doing per-file SFTP reads. let script = r#" -cd ~/.openclaw/agents 2>/dev/null || exit 0 +cd ~/.openclaw/agents 2>/dev/null || { echo '[]'; exit 0; } now=$(date +%s) +sep="" echo "[" -first_agent=1 for agent_dir in */; do + [ -d "$agent_dir" ] || continue agent="${agent_dir%/}" - first_session=1 + # Sanitize agent name for JSON (escape backslash then double-quote) + safe_agent=$(printf '%s' "$agent" | sed 's/\\/\\\\/g; s/"/\\"/g') for kind in sessions sessions_archive; do dir="$agent_dir$kind" [ -d "$dir" ] || continue for f in "$dir"/*.jsonl; do [ -f "$f" ] || continue fname=$(basename "$f" .jsonl) + safe_fname=$(printf '%s' "$fname" | sed 's/\\/\\\\/g; s/"/\\"/g') size=$(wc -c < "$f" 2>/dev/null | tr -d ' ') - msgs=$(grep -c '"type":"message"' "$f" 2>/dev/null || echo 0) - user_msgs=$(grep -c '"role":"user"' "$f" 2>/dev/null || echo 0) - asst_msgs=$(grep -c '"role":"assistant"' "$f" 2>/dev/null || echo 0) + msgs=$(grep -c '"type":"message"' "$f" 2>/dev/null || true) + [ -z "$msgs" ] && msgs=0 + user_msgs=$(grep -c '"role":"user"' "$f" 2>/dev/null || true) + [ -z "$user_msgs" ] && user_msgs=0 + asst_msgs=$(grep -c '"role":"assistant"' "$f" 2>/dev/null || true) + [ -z "$asst_msgs" ] && asst_msgs=0 mtime=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null || echo 0) age_days=$(( (now - mtime) / 86400 )) - if [ "$first_agent" = "1" ] && [ "$first_session" = "1" ]; then first_agent=0; else echo ","; fi - first_session=0 - printf '{"agent":"%s","sessionId":"%s","sizeBytes":%s,"messageCount":%s,"userMessageCount":%s,"assistantMessageCount":%s,"ageDays":%s,"kind":"%s"}' \ - "$agent" "$fname" "$size" "$msgs" "$user_msgs" "$asst_msgs" "$age_days" "$kind" + printf '%s{"agent":"%s","sessionId":"%s","sizeBytes":%s,"messageCount":%s,"userMessageCount":%s,"assistantMessageCount":%s,"ageDays":%s,"kind":"%s"}' \ + "$sep" "$safe_agent" "$safe_fname" "$size" "$msgs" "$user_msgs" "$asst_msgs" "$age_days" "$kind" + sep="," done done done diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index be20c4d..92c26de 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -25,7 +25,8 @@ use crate::commands::{ remote_restart_gateway, remote_apply_config_patch, remote_create_agent, remote_delete_agent, remote_assign_channel_agent, remote_set_global_model, remote_set_agent_model, - remote_run_doctor, remote_list_history, remote_list_discord_guild_channels, remote_write_raw_config, + remote_run_doctor, remote_list_history, remote_preview_rollback, remote_rollback, + remote_list_discord_guild_channels, remote_write_raw_config, remote_analyze_sessions, remote_delete_sessions_by_ids, remote_list_session_files, remote_clear_all_sessions, remote_preview_session, remote_list_model_profiles, remote_upsert_model_profile, remote_delete_model_profile, remote_resolve_api_keys, @@ -132,6 +133,8 @@ pub fn run() { remote_set_agent_model, remote_run_doctor, remote_list_history, + remote_preview_rollback, + remote_rollback, remote_list_discord_guild_channels, remote_write_raw_config, remote_analyze_sessions, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index df020b7..c5a1174 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,5 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2.0.0", + "productName": "ClawPal", "identifier": "xyz.clawpal", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/lib/api.ts b/src/lib/api.ts index 3a90206..0be511f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -168,6 +168,10 @@ export const api = { invoke("remote_run_doctor", { hostId }), remoteListHistory: (hostId: string): Promise<{ items: HistoryItem[] }> => invoke("remote_list_history", { hostId }), + remotePreviewRollback: (hostId: string, snapshotId: string): Promise => + invoke("remote_preview_rollback", { hostId, snapshotId }), + remoteRollback: (hostId: string, snapshotId: string): Promise => + invoke("remote_rollback", { hostId, snapshotId }), remoteWriteRawConfig: (hostId: string, content: string): Promise => invoke("remote_write_raw_config", { hostId, content }), remoteAnalyzeSessions: (hostId: string): Promise => diff --git a/src/pages/History.tsx b/src/pages/History.tsx index f165810..ced3e7d 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -73,14 +73,16 @@ export function History() { not rollbackable )} - {!isRollback && !isRemote && ( + {!isRollback && (