fix: comprehensive remote/local parity review fixes
- Add RemoteSystemStatus & Binding types for type safety (no more unsafe casts) - Add dedicated password field to SshHost (was reusing keyPath) - Fix remote_read_raw_config to return String instead of parsed JSON Value - Fix remote_list_history timestamps: convert Unix epoch to ISO 8601 - Add !isConnected guards: Chat send, Home delete agent, App apply changes - Fix History fallthrough to local API when remote but disconnected - Fix Doctor: runDoctorCmd rejects when remote+disconnected, listBackups skips fetch for remote, remove dead memoryFiles/totalMemoryBytes code - Fix Channels: add isRemote guard to refresh handlers, remove local Binding interface in favor of shared type, remove unsafe casts - Fix Chat: reset agentId on instance switch, add isConnected to deps - Fix App.tsx: narrow connectionStatus dependency to isConnected only - Use shared formatBytes from utils instead of local copy in Doctor - CreateAgentDialog uses useInstance() context instead of props 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:
@@ -1175,9 +1175,7 @@ fn expand_tilde(path: &str) -> String {
|
|||||||
path.to_string()
|
path.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_identity_md(workspace: &str) -> Option<(Option<String>, Option<String>)> {
|
fn parse_identity_content(content: &str) -> (Option<String>, Option<String>) {
|
||||||
let identity_path = std::path::Path::new(workspace).join("IDENTITY.md");
|
|
||||||
let content = fs::read_to_string(&identity_path).ok()?;
|
|
||||||
let mut name = None;
|
let mut name = None;
|
||||||
let mut emoji = None;
|
let mut emoji = None;
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
@@ -1196,7 +1194,13 @@ fn parse_identity_md(workspace: &str) -> Option<(Option<String>, Option<String>)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some((name, emoji))
|
(name, emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_identity_md(workspace: &str) -> Option<(Option<String>, Option<String>)> {
|
||||||
|
let identity_path = std::path::Path::new(workspace).join("IDENTITY.md");
|
||||||
|
let content = fs::read_to_string(&identity_path).ok()?;
|
||||||
|
Some(parse_identity_content(&content))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -4153,9 +4157,8 @@ pub async fn sftp_remove_file(pool: State<'_, SshConnectionPool>, host_id: Strin
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn remote_read_raw_config(pool: State<'_, SshConnectionPool>, host_id: String) -> Result<Value, String> {
|
pub async fn remote_read_raw_config(pool: State<'_, SshConnectionPool>, host_id: String) -> Result<String, String> {
|
||||||
let raw = pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await?;
|
pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await
|
||||||
serde_json::from_str(&raw).map_err(|e| format!("Failed to parse remote config: {e}"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -4243,7 +4246,7 @@ pub async fn remote_check_openclaw_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn remote_list_agents_overview(pool: State<'_, SshConnectionPool>, host_id: String) -> Result<Vec<Value>, String> {
|
pub async fn remote_list_agents_overview(pool: State<'_, SshConnectionPool>, host_id: String) -> Result<Vec<AgentOverview>, String> {
|
||||||
let raw = pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await?;
|
let raw = pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await?;
|
||||||
let cfg: Value = serde_json::from_str(&raw).map_err(|e| format!("Failed to parse remote config: {e}"))?;
|
let cfg: Value = serde_json::from_str(&raw).map_err(|e| format!("Failed to parse remote config: {e}"))?;
|
||||||
|
|
||||||
@@ -4253,23 +4256,51 @@ pub async fn remote_list_agents_overview(pool: State<'_, SshConnectionPool>, hos
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
let mut agents = Vec::new();
|
let mut agents = Vec::new();
|
||||||
|
let mut seen_ids = std::collections::HashSet::new();
|
||||||
let default_model = cfg.pointer("/agents/defaults/model")
|
let default_model = cfg.pointer("/agents/defaults/model")
|
||||||
.and_then(read_model_value)
|
.and_then(read_model_value)
|
||||||
.or_else(|| cfg.pointer("/agents/default/model").and_then(read_model_value));
|
.or_else(|| cfg.pointer("/agents/default/model").and_then(read_model_value));
|
||||||
|
let default_workspace = cfg.pointer("/agents/defaults/workspace")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
let channel_nodes = collect_channel_nodes(&cfg);
|
||||||
|
|
||||||
if let Some(list) = cfg.pointer("/agents/list").and_then(Value::as_array) {
|
if let Some(list) = cfg.pointer("/agents/list").and_then(Value::as_array) {
|
||||||
for agent in list {
|
for agent in list {
|
||||||
let id = agent.get("id").and_then(Value::as_str).unwrap_or("agent").to_string();
|
let id = agent.get("id").and_then(Value::as_str).unwrap_or("agent").to_string();
|
||||||
let model = agent.get("model").and_then(read_model_value).or_else(|| default_model.clone());
|
if !seen_ids.insert(id.clone()) {
|
||||||
agents.push(serde_json::json!({
|
continue;
|
||||||
"id": id,
|
}
|
||||||
"name": null,
|
|
||||||
"emoji": null,
|
let workspace = agent.get("workspace").and_then(Value::as_str)
|
||||||
"model": model,
|
.map(|s| s.to_string())
|
||||||
"channels": [],
|
.or_else(|| default_workspace.clone());
|
||||||
"online": gateway_running,
|
|
||||||
"workspace": null,
|
// Read IDENTITY.md from remote workspace via SFTP
|
||||||
}));
|
let (name, emoji) = if let Some(ref ws) = workspace {
|
||||||
|
let identity_path = format!("{}/IDENTITY.md", ws.trim_end_matches('/'));
|
||||||
|
match pool.sftp_read(&host_id, &identity_path).await {
|
||||||
|
Ok(content) => parse_identity_content(&content),
|
||||||
|
Err(_) => (None, None),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
let model = agent.get("model").and_then(read_model_value)
|
||||||
|
.or_else(|| default_model.clone());
|
||||||
|
let channels: Vec<String> = channel_nodes.iter()
|
||||||
|
.map(|ch| ch.path.clone())
|
||||||
|
.collect();
|
||||||
|
agents.push(AgentOverview {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
emoji,
|
||||||
|
model,
|
||||||
|
channels,
|
||||||
|
online: gateway_running,
|
||||||
|
workspace,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(agents)
|
Ok(agents)
|
||||||
@@ -4286,30 +4317,11 @@ pub async fn remote_list_channels_minimal(pool: State<'_, SshConnectionPool>, ho
|
|||||||
pub async fn remote_list_bindings(pool: State<'_, SshConnectionPool>, host_id: String) -> Result<Vec<Value>, String> {
|
pub async fn remote_list_bindings(pool: State<'_, SshConnectionPool>, host_id: String) -> Result<Vec<Value>, String> {
|
||||||
let raw = pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await?;
|
let raw = pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await?;
|
||||||
let cfg: Value = serde_json::from_str(&raw).map_err(|e| format!("Failed to parse remote config: {e}"))?;
|
let cfg: Value = serde_json::from_str(&raw).map_err(|e| format!("Failed to parse remote config: {e}"))?;
|
||||||
|
let bindings = cfg
|
||||||
let mut bindings = Vec::new();
|
.get("bindings")
|
||||||
if let Some(list) = cfg.pointer("/agents/list").and_then(Value::as_array) {
|
.and_then(Value::as_array)
|
||||||
for agent in list {
|
.cloned()
|
||||||
let agent_id = agent.get("id").and_then(Value::as_str).unwrap_or("").to_string();
|
.unwrap_or_default();
|
||||||
if let Some(channels_arr) = agent.get("channels").and_then(Value::as_array) {
|
|
||||||
for ch in channels_arr {
|
|
||||||
if let Some(ch_obj) = ch.as_object() {
|
|
||||||
let channel = ch_obj.get("channel").and_then(Value::as_str).unwrap_or("").to_string();
|
|
||||||
let peer = ch_obj.get("peer");
|
|
||||||
let peer_id = peer.and_then(|p| p.get("id")).and_then(Value::as_str).unwrap_or("").to_string();
|
|
||||||
let peer_kind = peer.and_then(|p| p.get("kind")).and_then(Value::as_str).unwrap_or("").to_string();
|
|
||||||
bindings.push(serde_json::json!({
|
|
||||||
"agentId": agent_id,
|
|
||||||
"match": {
|
|
||||||
"channel": channel,
|
|
||||||
"peer": { "id": peer_id, "kind": peer_kind }
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(bindings)
|
Ok(bindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4683,18 +4695,22 @@ pub async fn remote_list_history(
|
|||||||
// Parse filename: {timestamp}-{source}.json
|
// Parse filename: {timestamp}-{source}.json
|
||||||
let stem = entry.name.trim_end_matches(".json");
|
let stem = entry.name.trim_end_matches(".json");
|
||||||
let (ts_str, source) = stem.split_once('-').unwrap_or((stem, "unknown"));
|
let (ts_str, source) = stem.split_once('-').unwrap_or((stem, "unknown"));
|
||||||
let created_at = ts_str.parse::<u64>().unwrap_or(0);
|
let created_at = ts_str.parse::<i64>().unwrap_or(0);
|
||||||
|
// Convert Unix timestamp to ISO 8601 format for frontend compatibility
|
||||||
|
let created_at_iso = chrono::DateTime::from_timestamp(created_at, 0)
|
||||||
|
.map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
|
||||||
|
.unwrap_or_else(|| created_at.to_string());
|
||||||
items.push(serde_json::json!({
|
items.push(serde_json::json!({
|
||||||
"id": entry.name,
|
"id": entry.name,
|
||||||
"createdAt": created_at.to_string(),
|
"createdAt": created_at_iso,
|
||||||
"source": source,
|
"source": source,
|
||||||
"canRollback": true,
|
"canRollback": false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// Sort newest first
|
// Sort newest first
|
||||||
items.sort_by(|a, b| {
|
items.sort_by(|a, b| {
|
||||||
let ta = a["createdAt"].as_str().unwrap_or("0");
|
let ta = a["createdAt"].as_str().unwrap_or("");
|
||||||
let tb = b["createdAt"].as_str().unwrap_or("0");
|
let tb = b["createdAt"].as_str().unwrap_or("");
|
||||||
tb.cmp(ta)
|
tb.cmp(ta)
|
||||||
});
|
});
|
||||||
Ok(serde_json::json!({ "items": items }))
|
Ok(serde_json::json!({ "items": items }))
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub struct SshHostConfig {
|
|||||||
/// "key" | "ssh_config" | "password"
|
/// "key" | "ssh_config" | "password"
|
||||||
pub auth_method: String,
|
pub auth_method: String,
|
||||||
pub key_path: Option<String>,
|
pub key_path: Option<String>,
|
||||||
|
pub password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -175,8 +176,9 @@ impl SshConnectionPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"password" => {
|
"password" => {
|
||||||
// Password auth — password stored in key_path field for now
|
let password = config.password.as_deref()
|
||||||
let password = config.key_path.as_deref().unwrap_or("");
|
.or(config.key_path.as_deref()) // backwards compat
|
||||||
|
.unwrap_or("");
|
||||||
session
|
session
|
||||||
.authenticate_password(&username, password)
|
.authenticate_password(&username, password)
|
||||||
.await
|
.await
|
||||||
|
|||||||
10
src/App.tsx
10
src/App.tsx
@@ -116,7 +116,7 @@ export function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDirty(false); // Reset dirty on instance change
|
setDirty(false); // Reset dirty on instance change
|
||||||
checkDirty();
|
checkDirty();
|
||||||
pollRef.current = setInterval(checkDirty, 2000);
|
pollRef.current = setInterval(checkDirty, isRemote ? 5000 : 2000);
|
||||||
return () => {
|
return () => {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (pollRef.current) clearInterval(pollRef.current);
|
||||||
};
|
};
|
||||||
@@ -124,6 +124,7 @@ export function App() {
|
|||||||
|
|
||||||
// Load Discord data + extract profiles on startup or instance change
|
// Load Discord data + extract profiles on startup or instance change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setDiscordGuildChannels([]);
|
||||||
if (activeInstance === "local") {
|
if (activeInstance === "local") {
|
||||||
if (!localStorage.getItem("clawpal_profiles_extracted")) {
|
if (!localStorage.getItem("clawpal_profiles_extracted")) {
|
||||||
api.extractModelProfilesFromConfig()
|
api.extractModelProfilesFromConfig()
|
||||||
@@ -131,16 +132,17 @@ export function App() {
|
|||||||
.catch((e) => console.error("Failed to extract model profiles:", e));
|
.catch((e) => console.error("Failed to extract model profiles:", e));
|
||||||
}
|
}
|
||||||
api.listDiscordGuildChannels().then(setDiscordGuildChannels).catch((e) => console.error("Failed to load Discord channels:", e));
|
api.listDiscordGuildChannels().then(setDiscordGuildChannels).catch((e) => console.error("Failed to load Discord channels:", e));
|
||||||
} else if (connectionStatus[activeInstance] === "connected") {
|
} else if (isConnected) {
|
||||||
api.remoteListDiscordGuildChannels(activeInstance).then(setDiscordGuildChannels).catch((e) => console.error("Failed to load remote Discord channels:", e));
|
api.remoteListDiscordGuildChannels(activeInstance).then(setDiscordGuildChannels).catch((e) => console.error("Failed to load remote Discord channels:", e));
|
||||||
}
|
}
|
||||||
}, [activeInstance, connectionStatus]);
|
}, [activeInstance, isConnected]);
|
||||||
|
|
||||||
const bumpConfigVersion = useCallback(() => {
|
const bumpConfigVersion = useCallback(() => {
|
||||||
setConfigVersion((v) => v + 1);
|
setConfigVersion((v) => v + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleApplyClick = () => {
|
const handleApplyClick = () => {
|
||||||
|
if (isRemote && !isConnected) return;
|
||||||
// Load diff data for the dialog
|
// Load diff data for the dialog
|
||||||
const checkPromise = isRemote
|
const checkPromise = isRemote
|
||||||
? api.remoteCheckConfigDirty(activeInstance)
|
? api.remoteCheckConfigDirty(activeInstance)
|
||||||
@@ -354,7 +356,6 @@ export function App() {
|
|||||||
<Settings key={`${activeInstance}-${configVersion}`} onDataChange={bumpConfigVersion} />
|
<Settings key={`${activeInstance}-${configVersion}`} onDataChange={bumpConfigVersion} />
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</InstanceContext.Provider>
|
|
||||||
|
|
||||||
{/* Chat Panel -- inline, pushes main content */}
|
{/* Chat Panel -- inline, pushes main content */}
|
||||||
{chatOpen && (
|
{chatOpen && (
|
||||||
@@ -375,6 +376,7 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
</InstanceContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ interface Message {
|
|||||||
const AGENT_ID = "main";
|
const AGENT_ID = "main";
|
||||||
const SESSION_KEY_PREFIX = "clawpal_chat_session_";
|
const SESSION_KEY_PREFIX = "clawpal_chat_session_";
|
||||||
|
|
||||||
function loadSessionId(agent: string): string | undefined {
|
function loadSessionId(instanceId: string, agent: string): string | undefined {
|
||||||
return localStorage.getItem(SESSION_KEY_PREFIX + agent) || undefined;
|
return localStorage.getItem(SESSION_KEY_PREFIX + instanceId + "_" + agent) || undefined;
|
||||||
}
|
}
|
||||||
function saveSessionId(agent: string, sid: string) {
|
function saveSessionId(instanceId: string, agent: string, sid: string) {
|
||||||
localStorage.setItem(SESSION_KEY_PREFIX + agent, sid);
|
localStorage.setItem(SESSION_KEY_PREFIX + instanceId + "_" + agent, sid);
|
||||||
}
|
}
|
||||||
function clearSessionId(agent: string) {
|
function clearSessionId(instanceId: string, agent: string) {
|
||||||
localStorage.removeItem(SESSION_KEY_PREFIX + agent);
|
localStorage.removeItem(SESSION_KEY_PREFIX + instanceId + "_" + agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLAWPAL_CONTEXT = `[ClawPal Context] You are responding inside ClawPal, a desktop GUI for OpenClaw configuration.
|
const CLAWPAL_CONTEXT = `[ClawPal Context] You are responding inside ClawPal, a desktop GUI for OpenClaw configuration.
|
||||||
@@ -46,9 +46,15 @@ export function Chat() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [agents, setAgents] = useState<string[]>([]);
|
const [agents, setAgents] = useState<string[]>([]);
|
||||||
const [agentId, setAgentId] = useState(AGENT_ID);
|
const [agentId, setAgentId] = useState(AGENT_ID);
|
||||||
const [sessionId, setSessionId] = useState<string | undefined>(() => loadSessionId(AGENT_ID));
|
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMessages([]);
|
||||||
|
setAgentId(AGENT_ID);
|
||||||
|
setSessionId(loadSessionId(instanceId, AGENT_ID));
|
||||||
|
}, [instanceId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
@@ -66,6 +72,7 @@ export function Chat() {
|
|||||||
|
|
||||||
const send = useCallback(async () => {
|
const send = useCallback(async () => {
|
||||||
if (!input.trim() || loading) return;
|
if (!input.trim() || loading) return;
|
||||||
|
if (isRemote && !isConnected) return;
|
||||||
|
|
||||||
const userMsg: Message = { role: "user", content: input.trim() };
|
const userMsg: Message = { role: "user", content: input.trim() };
|
||||||
setMessages((prev) => [...prev, userMsg]);
|
setMessages((prev) => [...prev, userMsg]);
|
||||||
@@ -84,7 +91,7 @@ export function Chat() {
|
|||||||
if (agentMeta?.sessionId) {
|
if (agentMeta?.sessionId) {
|
||||||
const sid = agentMeta.sessionId as string;
|
const sid = agentMeta.sessionId as string;
|
||||||
setSessionId(sid);
|
setSessionId(sid);
|
||||||
saveSessionId(agentId, sid);
|
saveSessionId(instanceId, agentId, sid);
|
||||||
}
|
}
|
||||||
// Extract reply text
|
// Extract reply text
|
||||||
const payloads = result.payloads as Array<{ text?: string }> | undefined;
|
const payloads = result.payloads as Array<{ text?: string }> | undefined;
|
||||||
@@ -95,12 +102,12 @@ export function Chat() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [input, loading, agentId, sessionId, isRemote, instanceId]);
|
}, [input, loading, agentId, sessionId, isRemote, isConnected, instanceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Select value={agentId} onValueChange={(a) => { setAgentId(a); setSessionId(loadSessionId(a)); setMessages([]); }}>
|
<Select value={agentId} onValueChange={(a) => { setAgentId(a); setSessionId(loadSessionId(instanceId, a)); setMessages([]); }}>
|
||||||
<SelectTrigger size="sm" className="w-auto text-xs">
|
<SelectTrigger size="sm" className="w-auto text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -114,7 +121,7 @@ export function Chat() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-xs opacity-70"
|
className="text-xs opacity-70"
|
||||||
onClick={() => { clearSessionId(agentId); setSessionId(undefined); setMessages([]); }}
|
onClick={() => { clearSessionId(instanceId, agentId); setSessionId(undefined); setMessages([]); }}
|
||||||
>
|
>
|
||||||
New
|
New
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useInstance } from "@/lib/instance-context";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -31,16 +32,13 @@ export function CreateAgentDialog({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
modelProfiles,
|
modelProfiles,
|
||||||
onCreated,
|
onCreated,
|
||||||
instanceId,
|
|
||||||
isRemote,
|
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
modelProfiles: ModelProfile[];
|
modelProfiles: ModelProfile[];
|
||||||
onCreated: (result: CreateAgentResult) => void;
|
onCreated: (result: CreateAgentResult) => void;
|
||||||
instanceId?: string;
|
|
||||||
isRemote?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
|
const { instanceId, isRemote } = useInstance();
|
||||||
const [agentId, setAgentId] = useState("");
|
const [agentId, setAgentId] = useState("");
|
||||||
const [model, setModel] = useState("");
|
const [model, setModel] = useState("");
|
||||||
const [independent, setIndependent] = useState(false);
|
const [independent, setIndependent] = useState(false);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const emptyHost: Omit<SshHost, "id"> = {
|
|||||||
username: "",
|
username: "",
|
||||||
authMethod: "ssh_config",
|
authMethod: "ssh_config",
|
||||||
keyPath: undefined,
|
keyPath: undefined,
|
||||||
|
password: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InstanceTabBar({
|
export function InstanceTabBar({
|
||||||
@@ -64,6 +65,7 @@ export function InstanceTabBar({
|
|||||||
username: host.username,
|
username: host.username,
|
||||||
authMethod: host.authMethod,
|
authMethod: host.authMethod,
|
||||||
keyPath: host.keyPath,
|
keyPath: host.keyPath,
|
||||||
|
password: host.password,
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
@@ -221,7 +223,8 @@ export function InstanceTabBar({
|
|||||||
setForm((f) => ({
|
setForm((f) => ({
|
||||||
...f,
|
...f,
|
||||||
authMethod: val as SshHost["authMethod"],
|
authMethod: val as SshHost["authMethod"],
|
||||||
keyPath: val === "ssh_config" ? undefined : (val !== f.authMethod ? "" : f.keyPath),
|
keyPath: val === "key" ? (f.authMethod === "key" ? f.keyPath : "") : undefined,
|
||||||
|
password: val === "password" ? (f.authMethod === "password" ? f.password : "") : undefined,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -252,9 +255,12 @@ export function InstanceTabBar({
|
|||||||
<Input
|
<Input
|
||||||
id="ssh-password"
|
id="ssh-password"
|
||||||
type="password"
|
type="password"
|
||||||
value={form.keyPath || ""}
|
value={form.password || ""}
|
||||||
onChange={(e) => setForm((f) => ({ ...f, keyPath: e.target.value }))}
|
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-yellow-600 dark:text-yellow-400">
|
||||||
|
Password is stored in plaintext. Prefer SSH key or SSH config for better security.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { AgentOverview, AgentSessionAnalysis, ApplyResult, BackupInfo, ChannelNode, ConfigDirtyState, DiscordGuildChannel, HistoryItem, ModelCatalogProvider, ModelProfile, PreviewResult, ProviderAuthSuggestion, Recipe, ResolvedApiKey, StatusLight, SystemStatus, DoctorReport, MemoryFile, SessionFile, SshHost, SshExecResult, SftpEntry } from "./types";
|
import type { AgentOverview, AgentSessionAnalysis, ApplyResult, BackupInfo, Binding, ChannelNode, ConfigDirtyState, DiscordGuildChannel, HistoryItem, ModelCatalogProvider, ModelProfile, PreviewResult, ProviderAuthSuggestion, Recipe, RemoteSystemStatus, ResolvedApiKey, StatusLight, SystemStatus, DoctorReport, MemoryFile, SessionFile, SshHost, SshExecResult, SftpEntry } from "./types";
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
getSystemStatus: (): Promise<SystemStatus> =>
|
getSystemStatus: (): Promise<SystemStatus> =>
|
||||||
@@ -94,7 +94,7 @@ export const api = {
|
|||||||
invoke("restart_gateway", {}),
|
invoke("restart_gateway", {}),
|
||||||
setGlobalModel: (profileId: string | null): Promise<boolean> =>
|
setGlobalModel: (profileId: string | null): Promise<boolean> =>
|
||||||
invoke("set_global_model", { profileId }),
|
invoke("set_global_model", { profileId }),
|
||||||
listBindings: (): Promise<Record<string, unknown>[]> =>
|
listBindings: (): Promise<Binding[]> =>
|
||||||
invoke("list_bindings", {}),
|
invoke("list_bindings", {}),
|
||||||
assignChannelAgent: (channelType: string, peerId: string, agentId: string | null): Promise<boolean> =>
|
assignChannelAgent: (channelType: string, peerId: string, agentId: string | null): Promise<boolean> =>
|
||||||
invoke("assign_channel_agent", { channelType, peerId, agentId }),
|
invoke("assign_channel_agent", { channelType, peerId, agentId }),
|
||||||
@@ -136,15 +136,15 @@ export const api = {
|
|||||||
invoke("sftp_remove_file", { hostId, path }),
|
invoke("sftp_remove_file", { hostId, path }),
|
||||||
|
|
||||||
// Remote business commands
|
// Remote business commands
|
||||||
remoteReadRawConfig: (hostId: string): Promise<unknown> =>
|
remoteReadRawConfig: (hostId: string): Promise<string> =>
|
||||||
invoke("remote_read_raw_config", { hostId }),
|
invoke("remote_read_raw_config", { hostId }),
|
||||||
remoteGetSystemStatus: (hostId: string): Promise<Record<string, unknown>> =>
|
remoteGetSystemStatus: (hostId: string): Promise<RemoteSystemStatus> =>
|
||||||
invoke("remote_get_system_status", { hostId }),
|
invoke("remote_get_system_status", { hostId }),
|
||||||
remoteListAgentsOverview: (hostId: string): Promise<AgentOverview[]> =>
|
remoteListAgentsOverview: (hostId: string): Promise<AgentOverview[]> =>
|
||||||
invoke("remote_list_agents_overview", { hostId }),
|
invoke("remote_list_agents_overview", { hostId }),
|
||||||
remoteListChannelsMinimal: (hostId: string): Promise<ChannelNode[]> =>
|
remoteListChannelsMinimal: (hostId: string): Promise<ChannelNode[]> =>
|
||||||
invoke("remote_list_channels_minimal", { hostId }),
|
invoke("remote_list_channels_minimal", { hostId }),
|
||||||
remoteListBindings: (hostId: string): Promise<Record<string, unknown>[]> =>
|
remoteListBindings: (hostId: string): Promise<Binding[]> =>
|
||||||
invoke("remote_list_bindings", { hostId }),
|
invoke("remote_list_bindings", { hostId }),
|
||||||
remoteRestartGateway: (hostId: string): Promise<boolean> =>
|
remoteRestartGateway: (hostId: string): Promise<boolean> =>
|
||||||
invoke("remote_restart_gateway", { hostId }),
|
invoke("remote_restart_gateway", { hostId }),
|
||||||
@@ -190,7 +190,7 @@ export const api = {
|
|||||||
invoke("remote_refresh_model_catalog", { hostId }),
|
invoke("remote_refresh_model_catalog", { hostId }),
|
||||||
remoteChatViaOpenclaw: (hostId: string, agentId: string, message: string, sessionId?: string): Promise<Record<string, unknown>> =>
|
remoteChatViaOpenclaw: (hostId: string, agentId: string, message: string, sessionId?: string): Promise<Record<string, unknown>> =>
|
||||||
invoke("remote_chat_via_openclaw", { hostId, agentId, message, sessionId }),
|
invoke("remote_chat_via_openclaw", { hostId, agentId, message, sessionId }),
|
||||||
remoteCheckOpeclawUpdate: (hostId: string): Promise<{ upgradeAvailable: boolean; latestVersion: string | null; installedVersion: string }> =>
|
remoteCheckOpenclawUpdate: (hostId: string): Promise<{ upgradeAvailable: boolean; latestVersion: string | null; installedVersion: string }> =>
|
||||||
invoke("remote_check_openclaw_update", { hostId }),
|
invoke("remote_check_openclaw_update", { hostId }),
|
||||||
remoteSaveConfigBaseline: (hostId: string): Promise<boolean> =>
|
remoteSaveConfigBaseline: (hostId: string): Promise<boolean> =>
|
||||||
invoke("remote_save_config_baseline", { hostId }),
|
invoke("remote_save_config_baseline", { hostId }),
|
||||||
|
|||||||
@@ -230,6 +230,20 @@ export interface StatusLight {
|
|||||||
globalDefaultModel?: string;
|
globalDefaultModel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RemoteSystemStatus {
|
||||||
|
healthy: boolean;
|
||||||
|
openclawVersion: string;
|
||||||
|
activeAgents: number;
|
||||||
|
globalDefaultModel?: string;
|
||||||
|
configPath: string;
|
||||||
|
openclawDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Binding {
|
||||||
|
agentId: string;
|
||||||
|
match: { channel: string; peer?: { id: string; kind: string } };
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConfigDirtyState {
|
export interface ConfigDirtyState {
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
baseline: string;
|
baseline: string;
|
||||||
@@ -251,6 +265,7 @@ export interface SshHost {
|
|||||||
username: string;
|
username: string;
|
||||||
authMethod: "key" | "ssh_config" | "password";
|
authMethod: "key" | "ssh_config" | "password";
|
||||||
keyPath?: string;
|
keyPath?: string;
|
||||||
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SshExecResult {
|
export interface SshExecResult {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import type { AgentOverview, ChannelNode, DiscordGuildChannel, ModelProfile } from "../lib/types";
|
import type { AgentOverview, Binding, ChannelNode, DiscordGuildChannel, ModelProfile } from "../lib/types";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { useInstance } from "@/lib/instance-context";
|
import { useInstance } from "@/lib/instance-context";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
@@ -17,11 +17,6 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { CreateAgentDialog, type CreateAgentResult } from "@/components/CreateAgentDialog";
|
import { CreateAgentDialog, type CreateAgentResult } from "@/components/CreateAgentDialog";
|
||||||
|
|
||||||
interface Binding {
|
|
||||||
agentId: string;
|
|
||||||
match: { channel: string; peer?: { id: string; kind: string } };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AgentGroup {
|
interface AgentGroup {
|
||||||
identity: string;
|
identity: string;
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
@@ -91,9 +86,9 @@ export function Channels({
|
|||||||
const refreshBindings = useCallback(() => {
|
const refreshBindings = useCallback(() => {
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
api.remoteListBindings(instanceId).then((b) => setBindings(b as unknown as Binding[])).catch((e) => console.error("Failed to load remote bindings:", e));
|
api.remoteListBindings(instanceId).then((b) => setBindings(b)).catch((e) => console.error("Failed to load remote bindings:", e));
|
||||||
} else {
|
} else {
|
||||||
api.listBindings().then((b) => setBindings(b as unknown as Binding[])).catch((e) => console.error("Failed to load bindings:", e));
|
api.listBindings().then((b) => setBindings(b)).catch((e) => console.error("Failed to load bindings:", e));
|
||||||
}
|
}
|
||||||
}, [isRemote, isConnected, instanceId]);
|
}, [isRemote, isConnected, instanceId]);
|
||||||
|
|
||||||
@@ -120,12 +115,15 @@ export function Channels({
|
|||||||
refreshBindings();
|
refreshBindings();
|
||||||
if (!isRemote) {
|
if (!isRemote) {
|
||||||
api.listModelProfiles().then((p) => setModelProfiles(p.filter((m) => m.enabled))).catch((e) => console.error("Failed to load model profiles:", e));
|
api.listModelProfiles().then((p) => setModelProfiles(p.filter((m) => m.enabled))).catch((e) => console.error("Failed to load model profiles:", e));
|
||||||
|
} else if (isRemote && isConnected) {
|
||||||
|
api.remoteListModelProfiles(instanceId).then((p) => setModelProfiles(p.filter((m) => m.enabled))).catch((e) => console.error("Failed to load remote model profiles:", e));
|
||||||
}
|
}
|
||||||
refreshChannelNodes();
|
refreshChannelNodes();
|
||||||
refreshDiscordCache();
|
refreshDiscordCache();
|
||||||
}, [isRemote, refreshAgents, refreshBindings, refreshChannelNodes, refreshDiscordCache]);
|
}, [isRemote, refreshAgents, refreshBindings, refreshChannelNodes, refreshDiscordCache]);
|
||||||
|
|
||||||
const handleRefreshDiscord = () => {
|
const handleRefreshDiscord = () => {
|
||||||
|
if (isRemote) return; // Refresh is local-only
|
||||||
setRefreshing("discord");
|
setRefreshing("discord");
|
||||||
api.refreshDiscordGuildChannels()
|
api.refreshDiscordGuildChannels()
|
||||||
.then((channels) => {
|
.then((channels) => {
|
||||||
@@ -137,6 +135,7 @@ export function Channels({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRefreshPlatform = (platform: string) => {
|
const handleRefreshPlatform = (platform: string) => {
|
||||||
|
if (isRemote) return; // Refresh is local-only
|
||||||
setRefreshing(platform);
|
setRefreshing(platform);
|
||||||
api.listChannelsMinimal()
|
api.listChannelsMinimal()
|
||||||
.then((nodes) => {
|
.then((nodes) => {
|
||||||
@@ -250,14 +249,12 @@ export function Channels({
|
|||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
))}
|
))}
|
||||||
{!isRemote && (
|
<>
|
||||||
<>
|
<SelectSeparator />
|
||||||
<SelectSeparator />
|
<SelectItem value="__new__">
|
||||||
<SelectItem value="__new__">
|
<span className="text-primary">+ New agent...</span>
|
||||||
<span className="text-primary">+ New agent...</span>
|
</SelectItem>
|
||||||
</SelectItem>
|
</>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
@@ -367,36 +364,33 @@ export function Channels({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create Agent Dialog — local only */}
|
<CreateAgentDialog
|
||||||
{!isRemote && (
|
open={showCreateAgent}
|
||||||
<CreateAgentDialog
|
onOpenChange={(open) => {
|
||||||
open={showCreateAgent}
|
setShowCreateAgent(open);
|
||||||
onOpenChange={(open) => {
|
if (!open) setPendingChannel(null);
|
||||||
setShowCreateAgent(open);
|
}}
|
||||||
if (!open) setPendingChannel(null);
|
modelProfiles={modelProfiles}
|
||||||
}}
|
onCreated={(result: CreateAgentResult) => {
|
||||||
modelProfiles={modelProfiles}
|
refreshAgents();
|
||||||
instanceId={instanceId}
|
if (pendingChannel) {
|
||||||
isRemote={isRemote}
|
handleAssign(pendingChannel.platform, pendingChannel.peerId, result.agentId);
|
||||||
onCreated={(result: CreateAgentResult) => {
|
if (result.persona && pendingChannel.platform === "discord") {
|
||||||
refreshAgents();
|
const ch = discordChannels.find((c) => c.channelId === pendingChannel.peerId);
|
||||||
if (pendingChannel) {
|
if (ch) {
|
||||||
handleAssign(pendingChannel.platform, pendingChannel.peerId, result.agentId);
|
const patch = JSON.stringify({
|
||||||
// Apply persona as systemPrompt on the channel if provided
|
channels: { discord: { guilds: { [ch.guildId]: { channels: { [ch.channelId]: { systemPrompt: result.persona } } } } } },
|
||||||
if (result.persona && pendingChannel.platform === "discord") {
|
});
|
||||||
const ch = discordChannels.find((c) => c.channelId === pendingChannel.peerId);
|
const patchPromise = isRemote
|
||||||
if (ch) {
|
? api.remoteApplyConfigPatch(instanceId, patch, {})
|
||||||
const patch = JSON.stringify({
|
: api.applyConfigPatch(patch, {});
|
||||||
channels: { discord: { guilds: { [ch.guildId]: { channels: { [ch.channelId]: { systemPrompt: result.persona } } } } } },
|
patchPromise.catch((e) => showToast?.(String(e), "error"));
|
||||||
});
|
|
||||||
api.applyConfigPatch(patch, {}).catch((e) => showToast?.(String(e), "error"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setPendingChannel(null);
|
|
||||||
}
|
}
|
||||||
}}
|
setPendingChannel(null);
|
||||||
/>
|
}
|
||||||
)}
|
}}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useReducer, useState } from "react";
|
|||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { useInstance } from "@/lib/instance-context";
|
import { useInstance } from "@/lib/instance-context";
|
||||||
import { initialState, reducer } from "@/lib/state";
|
import { initialState, reducer } from "@/lib/state";
|
||||||
import type { AgentSessionAnalysis, BackupInfo, MemoryFile, SessionFile } from "@/lib/types";
|
import { formatBytes } from "@/lib/utils";
|
||||||
|
import type { AgentSessionAnalysis, BackupInfo, SessionFile } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -30,23 +31,10 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
function formatBytes(bytes: number) {
|
|
||||||
if (bytes <= 0) return "0 B";
|
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
|
||||||
let index = 0;
|
|
||||||
let value = bytes;
|
|
||||||
while (value >= 1024 && index < units.length - 1) {
|
|
||||||
value /= 1024;
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
return `${value.toFixed(1)} ${units[index]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Doctor() {
|
export function Doctor() {
|
||||||
const { instanceId, isRemote, isConnected } = useInstance();
|
const { instanceId, isRemote, isConnected } = useInstance();
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const [rawOutput, setRawOutput] = useState<string | null>(null);
|
const [rawOutput, setRawOutput] = useState<string | null>(null);
|
||||||
const [memoryFiles, setMemoryFiles] = useState<MemoryFile[]>([]);
|
|
||||||
const [sessionFiles, setSessionFiles] = useState<SessionFile[]>([]);
|
const [sessionFiles, setSessionFiles] = useState<SessionFile[]>([]);
|
||||||
const [backups, setBackups] = useState<BackupInfo[]>([]);
|
const [backups, setBackups] = useState<BackupInfo[]>([]);
|
||||||
const [dataMessage, setDataMessage] = useState("");
|
const [dataMessage, setDataMessage] = useState("");
|
||||||
@@ -84,17 +72,14 @@ export function Doctor() {
|
|||||||
}));
|
}));
|
||||||
}, [sessionFiles]);
|
}, [sessionFiles]);
|
||||||
|
|
||||||
const totalMemoryBytes = useMemo(
|
|
||||||
() => memoryFiles.reduce((sum, f) => sum + f.sizeBytes, 0),
|
|
||||||
[memoryFiles],
|
|
||||||
);
|
|
||||||
const totalSessionBytes = useMemo(
|
const totalSessionBytes = useMemo(
|
||||||
() => sessionFiles.reduce((sum, f) => sum + f.sizeBytes, 0),
|
() => sessionFiles.reduce((sum, f) => sum + f.sizeBytes, 0),
|
||||||
[sessionFiles],
|
[sessionFiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
function runDoctorCmd(): Promise<import("@/lib/types").DoctorReport> {
|
function runDoctorCmd(): Promise<import("@/lib/types").DoctorReport> {
|
||||||
if (isRemote && isConnected) {
|
if (isRemote) {
|
||||||
|
if (!isConnected) return Promise.reject("Not connected");
|
||||||
return api.remoteRunDoctor(instanceId).then((report) => {
|
return api.remoteRunDoctor(instanceId).then((report) => {
|
||||||
// Remote doctor may return a rawOutput field instead of structured issues
|
// Remote doctor may return a rawOutput field instead of structured issues
|
||||||
const raw = (report as unknown as Record<string, unknown>).rawOutput;
|
const raw = (report as unknown as Record<string, unknown>).rawOutput;
|
||||||
@@ -117,9 +102,6 @@ export function Doctor() {
|
|||||||
.then(setSessionFiles)
|
.then(setSessionFiles)
|
||||||
.catch(() => setDataMessage("Failed to load remote session files"));
|
.catch(() => setDataMessage("Failed to load remote session files"));
|
||||||
} else {
|
} else {
|
||||||
api.listMemoryFiles()
|
|
||||||
.then(setMemoryFiles)
|
|
||||||
.catch(() => setDataMessage("Failed to load memory files"));
|
|
||||||
api.listSessionFiles()
|
api.listSessionFiles()
|
||||||
.then(setSessionFiles)
|
.then(setSessionFiles)
|
||||||
.catch(() => setDataMessage("Failed to load session files"));
|
.catch(() => setDataMessage("Failed to load session files"));
|
||||||
@@ -156,11 +138,13 @@ export function Doctor() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
refreshData();
|
refreshData();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [instanceId, isRemote, isConnected]);
|
}, [instanceId, isRemote, isConnected]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isRemote) { setBackups([]); return; }
|
||||||
api.listBackups().then(setBackups).catch((e) => console.error("Failed to load backups:", e));
|
api.listBackups().then(setBackups).catch((e) => console.error("Failed to load backups:", e));
|
||||||
}, []);
|
}, [instanceId, isRemote]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ export function History() {
|
|||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
const refreshHistory = () => {
|
const refreshHistory = () => {
|
||||||
const promise = isRemote && isConnected
|
if (isRemote) {
|
||||||
? api.remoteListHistory(instanceId)
|
if (!isConnected) return;
|
||||||
: api.listHistory(50, 0);
|
return api.remoteListHistory(instanceId)
|
||||||
return promise
|
.then((resp) => setHistory(resp.items))
|
||||||
|
.catch(() => setMessage("Failed to load history"));
|
||||||
|
}
|
||||||
|
return api.listHistory(50, 0)
|
||||||
.then((resp) => setHistory(resp.items))
|
.then((resp) => setHistory(resp.items))
|
||||||
.catch(() => setMessage("Failed to load history"));
|
.catch(() => setMessage("Failed to load history"));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
import { CreateAgentDialog } from "@/components/CreateAgentDialog";
|
import { CreateAgentDialog } from "@/components/CreateAgentDialog";
|
||||||
import { RecipeCard } from "@/components/RecipeCard";
|
import { RecipeCard } from "@/components/RecipeCard";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import type { StatusLight, AgentOverview, Recipe, BackupInfo, ModelProfile } from "../lib/types";
|
import type { StatusLight, AgentOverview, Recipe, BackupInfo, ModelProfile, RemoteSystemStatus } from "../lib/types";
|
||||||
import { formatTime, formatBytes } from "@/lib/utils";
|
import { formatTime, formatBytes } from "@/lib/utils";
|
||||||
import { useInstance } from "@/lib/instance-context";
|
import { useInstance } from "@/lib/instance-context";
|
||||||
|
|
||||||
@@ -80,13 +80,10 @@ export function Home({
|
|||||||
const fetchStatus = useCallback(() => {
|
const fetchStatus = useCallback(() => {
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
if (!isConnected) return; // Wait for SSH connection
|
if (!isConnected) return; // Wait for SSH connection
|
||||||
api.remoteGetSystemStatus(instanceId).then((s) => {
|
api.remoteGetSystemStatus(instanceId).then((s: RemoteSystemStatus) => {
|
||||||
const healthy = s.healthy as boolean ?? false;
|
setStatus({ healthy: s.healthy, activeAgents: s.activeAgents, globalDefaultModel: s.globalDefaultModel });
|
||||||
const activeAgents = s.activeAgents as number ?? 0;
|
|
||||||
const globalDefaultModel = s.globalDefaultModel as string | undefined;
|
|
||||||
setStatus({ healthy, activeAgents, globalDefaultModel });
|
|
||||||
setStatusSettled(true);
|
setStatusSettled(true);
|
||||||
if (s.openclawVersion) setVersion(s.openclawVersion as string);
|
if (s.openclawVersion) setVersion(s.openclawVersion);
|
||||||
}).catch((e) => console.error("Failed to fetch remote status:", e));
|
}).catch((e) => console.error("Failed to fetch remote status:", e));
|
||||||
} else {
|
} else {
|
||||||
api.getStatusLight().then((s) => {
|
api.getStatusLight().then((s) => {
|
||||||
@@ -167,7 +164,7 @@ export function Home({
|
|||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
api.remoteCheckOpeclawUpdate(instanceId).then((u) => {
|
api.remoteCheckOpenclawUpdate(instanceId).then((u) => {
|
||||||
setUpdateInfo({ available: u.upgradeAvailable, latest: u.latestVersion ?? undefined });
|
setUpdateInfo({ available: u.upgradeAvailable, latest: u.latestVersion ?? undefined });
|
||||||
}).catch((e) => console.error("Failed to check remote update:", e));
|
}).catch((e) => console.error("Failed to check remote update:", e));
|
||||||
} else {
|
} else {
|
||||||
@@ -186,6 +183,7 @@ export function Home({
|
|||||||
}, [isRemote, isConnected, instanceId]);
|
}, [isRemote, isConnected, instanceId]);
|
||||||
|
|
||||||
const handleDeleteAgent = (agentId: string) => {
|
const handleDeleteAgent = (agentId: string) => {
|
||||||
|
if (isRemote && !isConnected) return;
|
||||||
const deletePromise = isRemote
|
const deletePromise = isRemote
|
||||||
? api.remoteDeleteAgent(instanceId, agentId)
|
? api.remoteDeleteAgent(instanceId, agentId)
|
||||||
: api.deleteAgent(agentId);
|
: api.deleteAgent(agentId);
|
||||||
@@ -526,8 +524,6 @@ export function Home({
|
|||||||
onOpenChange={setShowCreateAgent}
|
onOpenChange={setShowCreateAgent}
|
||||||
modelProfiles={modelProfiles}
|
modelProfiles={modelProfiles}
|
||||||
onCreated={() => refreshAgents()}
|
onCreated={() => refreshAgents()}
|
||||||
instanceId={instanceId}
|
|
||||||
isRemote={isRemote}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user