feat: remote rollback support, fix analyze sessions, set app product name
- Add remote_preview_rollback and remote_rollback Tauri commands - Enable rollback UI for remote instances in History page - Fix remote_analyze_sessions SSH script: JSON-escape agent/session names, fix comma logic, improve grep error resilience - Set productName to "ClawPal" in tauri.conf.json (was defaulting to lowercase) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -447,7 +447,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clawpal"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
|
||||
@@ -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<PreviewResult, String> {
|
||||
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<ApplyResult, String> {
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0",
|
||||
"productName": "ClawPal",
|
||||
"identifier": "xyz.clawpal",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -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<PreviewResult> =>
|
||||
invoke("remote_preview_rollback", { hostId, snapshotId }),
|
||||
remoteRollback: (hostId: string, snapshotId: string): Promise<ApplyResult> =>
|
||||
invoke("remote_rollback", { hostId, snapshotId }),
|
||||
remoteWriteRawConfig: (hostId: string, content: string): Promise<boolean> =>
|
||||
invoke("remote_write_raw_config", { hostId, content }),
|
||||
remoteAnalyzeSessions: (hostId: string): Promise<AgentSessionAnalysis[]> =>
|
||||
|
||||
@@ -73,14 +73,16 @@ export function History() {
|
||||
<Badge variant="outline" className="text-muted-foreground">not rollbackable</Badge>
|
||||
)}
|
||||
</div>
|
||||
{!isRollback && !isRemote && (
|
||||
{!isRollback && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const p = await api.previewRollback(item.id);
|
||||
const p = isRemote
|
||||
? await api.remotePreviewRollback(instanceId, item.id)
|
||||
: await api.previewRollback(item.id);
|
||||
setPreview(p);
|
||||
} catch (err) {
|
||||
setMessage(String(err));
|
||||
@@ -95,7 +97,11 @@ export function History() {
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await api.rollback(item.id);
|
||||
if (isRemote) {
|
||||
await api.remoteRollback(instanceId, item.id);
|
||||
} else {
|
||||
await api.rollback(item.id);
|
||||
}
|
||||
setMessage("Rollback completed");
|
||||
await refreshHistory();
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user