diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 370372c..0a2f429 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1175,9 +1175,7 @@ fn expand_tilde(path: &str) -> String { path.to_string() } -fn parse_identity_md(workspace: &str) -> Option<(Option, Option)> { - let identity_path = std::path::Path::new(workspace).join("IDENTITY.md"); - let content = fs::read_to_string(&identity_path).ok()?; +fn parse_identity_content(content: &str) -> (Option, Option) { let mut name = None; let mut emoji = None; for line in content.lines() { @@ -1196,7 +1194,13 @@ fn parse_identity_md(workspace: &str) -> Option<(Option, Option) } } } - Some((name, emoji)) + (name, emoji) +} + +fn parse_identity_md(workspace: &str) -> Option<(Option, Option)> { + 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] @@ -4153,9 +4157,8 @@ pub async fn sftp_remove_file(pool: State<'_, SshConnectionPool>, host_id: Strin // --------------------------------------------------------------------------- #[tauri::command] -pub async fn remote_read_raw_config(pool: State<'_, SshConnectionPool>, host_id: String) -> Result { - let raw = pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await?; - serde_json::from_str(&raw).map_err(|e| format!("Failed to parse remote config: {e}")) +pub async fn remote_read_raw_config(pool: State<'_, SshConnectionPool>, host_id: String) -> Result { + pool.sftp_read(&host_id, "~/.openclaw/openclaw.json").await } #[tauri::command] @@ -4243,7 +4246,7 @@ pub async fn remote_check_openclaw_update( } #[tauri::command] -pub async fn remote_list_agents_overview(pool: State<'_, SshConnectionPool>, host_id: String) -> Result, String> { +pub async fn remote_list_agents_overview(pool: State<'_, SshConnectionPool>, host_id: String) -> Result, String> { 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}"))?; @@ -4253,23 +4256,51 @@ pub async fn remote_list_agents_overview(pool: State<'_, SshConnectionPool>, hos .unwrap_or(false); let mut agents = Vec::new(); + let mut seen_ids = std::collections::HashSet::new(); let default_model = cfg.pointer("/agents/defaults/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) { for agent in list { 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()); - agents.push(serde_json::json!({ - "id": id, - "name": null, - "emoji": null, - "model": model, - "channels": [], - "online": gateway_running, - "workspace": null, - })); + if !seen_ids.insert(id.clone()) { + continue; + } + + let workspace = agent.get("workspace").and_then(Value::as_str) + .map(|s| s.to_string()) + .or_else(|| default_workspace.clone()); + + // 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 = channel_nodes.iter() + .map(|ch| ch.path.clone()) + .collect(); + agents.push(AgentOverview { + id, + name, + emoji, + model, + channels, + online: gateway_running, + workspace, + }); } } 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, String> { 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 mut bindings = Vec::new(); - if let Some(list) = cfg.pointer("/agents/list").and_then(Value::as_array) { - for agent in list { - let agent_id = agent.get("id").and_then(Value::as_str).unwrap_or("").to_string(); - 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 } - } - })); - } - } - } - } - } + let bindings = cfg + .get("bindings") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); Ok(bindings) } @@ -4683,18 +4695,22 @@ pub async fn remote_list_history( // Parse filename: {timestamp}-{source}.json let stem = entry.name.trim_end_matches(".json"); let (ts_str, source) = stem.split_once('-').unwrap_or((stem, "unknown")); - let created_at = ts_str.parse::().unwrap_or(0); + let created_at = ts_str.parse::().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!({ "id": entry.name, - "createdAt": created_at.to_string(), + "createdAt": created_at_iso, "source": source, - "canRollback": true, + "canRollback": false, })); } // Sort newest first items.sort_by(|a, b| { - let ta = a["createdAt"].as_str().unwrap_or("0"); - let tb = b["createdAt"].as_str().unwrap_or("0"); + let ta = a["createdAt"].as_str().unwrap_or(""); + let tb = b["createdAt"].as_str().unwrap_or(""); tb.cmp(ta) }); Ok(serde_json::json!({ "items": items })) diff --git a/src-tauri/src/ssh.rs b/src-tauri/src/ssh.rs index 8bb5ad4..9d876cd 100644 --- a/src-tauri/src/ssh.rs +++ b/src-tauri/src/ssh.rs @@ -24,6 +24,7 @@ pub struct SshHostConfig { /// "key" | "ssh_config" | "password" pub auth_method: String, pub key_path: Option, + pub password: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -175,8 +176,9 @@ impl SshConnectionPool { } } "password" => { - // Password auth — password stored in key_path field for now - let password = config.key_path.as_deref().unwrap_or(""); + let password = config.password.as_deref() + .or(config.key_path.as_deref()) // backwards compat + .unwrap_or(""); session .authenticate_password(&username, password) .await diff --git a/src/App.tsx b/src/App.tsx index 3aba08b..3c9de68 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -116,7 +116,7 @@ export function App() { useEffect(() => { setDirty(false); // Reset dirty on instance change checkDirty(); - pollRef.current = setInterval(checkDirty, 2000); + pollRef.current = setInterval(checkDirty, isRemote ? 5000 : 2000); return () => { if (pollRef.current) clearInterval(pollRef.current); }; @@ -124,6 +124,7 @@ export function App() { // Load Discord data + extract profiles on startup or instance change useEffect(() => { + setDiscordGuildChannels([]); if (activeInstance === "local") { if (!localStorage.getItem("clawpal_profiles_extracted")) { api.extractModelProfilesFromConfig() @@ -131,16 +132,17 @@ export function App() { .catch((e) => console.error("Failed to extract model profiles:", 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)); } - }, [activeInstance, connectionStatus]); + }, [activeInstance, isConnected]); const bumpConfigVersion = useCallback(() => { setConfigVersion((v) => v + 1); }, []); const handleApplyClick = () => { + if (isRemote && !isConnected) return; // Load diff data for the dialog const checkPromise = isRemote ? api.remoteCheckConfigDirty(activeInstance) @@ -354,7 +356,6 @@ export function App() { )} - {/* Chat Panel -- inline, pushes main content */} {chatOpen && ( @@ -375,6 +376,7 @@ export function App() { )} + diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 26950fc..5ceed00 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -21,14 +21,14 @@ interface Message { const AGENT_ID = "main"; const SESSION_KEY_PREFIX = "clawpal_chat_session_"; -function loadSessionId(agent: string): string | undefined { - return localStorage.getItem(SESSION_KEY_PREFIX + agent) || undefined; +function loadSessionId(instanceId: string, agent: string): string | undefined { + return localStorage.getItem(SESSION_KEY_PREFIX + instanceId + "_" + agent) || undefined; } -function saveSessionId(agent: string, sid: string) { - localStorage.setItem(SESSION_KEY_PREFIX + agent, sid); +function saveSessionId(instanceId: string, agent: string, sid: string) { + localStorage.setItem(SESSION_KEY_PREFIX + instanceId + "_" + agent, sid); } -function clearSessionId(agent: string) { - localStorage.removeItem(SESSION_KEY_PREFIX + agent); +function clearSessionId(instanceId: string, agent: string) { + localStorage.removeItem(SESSION_KEY_PREFIX + instanceId + "_" + agent); } 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 [agents, setAgents] = useState([]); const [agentId, setAgentId] = useState(AGENT_ID); - const [sessionId, setSessionId] = useState(() => loadSessionId(AGENT_ID)); + const [sessionId, setSessionId] = useState(undefined); const bottomRef = useRef(null); + useEffect(() => { + setMessages([]); + setAgentId(AGENT_ID); + setSessionId(loadSessionId(instanceId, AGENT_ID)); + }, [instanceId]); + useEffect(() => { if (isRemote) { if (!isConnected) return; @@ -66,6 +72,7 @@ export function Chat() { const send = useCallback(async () => { if (!input.trim() || loading) return; + if (isRemote && !isConnected) return; const userMsg: Message = { role: "user", content: input.trim() }; setMessages((prev) => [...prev, userMsg]); @@ -84,7 +91,7 @@ export function Chat() { if (agentMeta?.sessionId) { const sid = agentMeta.sessionId as string; setSessionId(sid); - saveSessionId(agentId, sid); + saveSessionId(instanceId, agentId, sid); } // Extract reply text const payloads = result.payloads as Array<{ text?: string }> | undefined; @@ -95,12 +102,12 @@ export function Chat() { } finally { setLoading(false); } - }, [input, loading, agentId, sessionId, isRemote, instanceId]); + }, [input, loading, agentId, sessionId, isRemote, isConnected, instanceId]); return (
- { setAgentId(a); setSessionId(loadSessionId(instanceId, a)); setMessages([]); }}> @@ -114,7 +121,7 @@ export function Chat() { variant="ghost" size="sm" className="text-xs opacity-70" - onClick={() => { clearSessionId(agentId); setSessionId(undefined); setMessages([]); }} + onClick={() => { clearSessionId(instanceId, agentId); setSessionId(undefined); setMessages([]); }} > New diff --git a/src/components/CreateAgentDialog.tsx b/src/components/CreateAgentDialog.tsx index 52f17b9..c4a32b1 100644 --- a/src/components/CreateAgentDialog.tsx +++ b/src/components/CreateAgentDialog.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useInstance } from "@/lib/instance-context"; import { api } from "../lib/api"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -31,16 +32,13 @@ export function CreateAgentDialog({ onOpenChange, modelProfiles, onCreated, - instanceId, - isRemote, }: { open: boolean; onOpenChange: (open: boolean) => void; modelProfiles: ModelProfile[]; onCreated: (result: CreateAgentResult) => void; - instanceId?: string; - isRemote?: boolean; }) { + const { instanceId, isRemote } = useInstance(); const [agentId, setAgentId] = useState(""); const [model, setModel] = useState(""); const [independent, setIndependent] = useState(false); diff --git a/src/components/InstanceTabBar.tsx b/src/components/InstanceTabBar.tsx index 50b7b76..7134fd1 100644 --- a/src/components/InstanceTabBar.tsx +++ b/src/components/InstanceTabBar.tsx @@ -35,6 +35,7 @@ const emptyHost: Omit = { username: "", authMethod: "ssh_config", keyPath: undefined, + password: undefined, }; export function InstanceTabBar({ @@ -64,6 +65,7 @@ export function InstanceTabBar({ username: host.username, authMethod: host.authMethod, keyPath: host.keyPath, + password: host.password, }); setDialogOpen(true); }; @@ -221,7 +223,8 @@ export function InstanceTabBar({ setForm((f) => ({ ...f, 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({ setForm((f) => ({ ...f, keyPath: e.target.value }))} + value={form.password || ""} + onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} /> +

+ Password is stored in plaintext. Prefer SSH key or SSH config for better security. +

)}
diff --git a/src/lib/api.ts b/src/lib/api.ts index 9aa7f34..a49f572 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,5 +1,5 @@ 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 = { getSystemStatus: (): Promise => @@ -94,7 +94,7 @@ export const api = { invoke("restart_gateway", {}), setGlobalModel: (profileId: string | null): Promise => invoke("set_global_model", { profileId }), - listBindings: (): Promise[]> => + listBindings: (): Promise => invoke("list_bindings", {}), assignChannelAgent: (channelType: string, peerId: string, agentId: string | null): Promise => invoke("assign_channel_agent", { channelType, peerId, agentId }), @@ -136,15 +136,15 @@ export const api = { invoke("sftp_remove_file", { hostId, path }), // Remote business commands - remoteReadRawConfig: (hostId: string): Promise => + remoteReadRawConfig: (hostId: string): Promise => invoke("remote_read_raw_config", { hostId }), - remoteGetSystemStatus: (hostId: string): Promise> => + remoteGetSystemStatus: (hostId: string): Promise => invoke("remote_get_system_status", { hostId }), remoteListAgentsOverview: (hostId: string): Promise => invoke("remote_list_agents_overview", { hostId }), remoteListChannelsMinimal: (hostId: string): Promise => invoke("remote_list_channels_minimal", { hostId }), - remoteListBindings: (hostId: string): Promise[]> => + remoteListBindings: (hostId: string): Promise => invoke("remote_list_bindings", { hostId }), remoteRestartGateway: (hostId: string): Promise => invoke("remote_restart_gateway", { hostId }), @@ -190,7 +190,7 @@ export const api = { invoke("remote_refresh_model_catalog", { hostId }), remoteChatViaOpenclaw: (hostId: string, agentId: string, message: string, sessionId?: string): Promise> => 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 }), remoteSaveConfigBaseline: (hostId: string): Promise => invoke("remote_save_config_baseline", { hostId }), diff --git a/src/lib/types.ts b/src/lib/types.ts index 1045d86..fe9ae47 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -230,6 +230,20 @@ export interface StatusLight { 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 { dirty: boolean; baseline: string; @@ -251,6 +265,7 @@ export interface SshHost { username: string; authMethod: "key" | "ssh_config" | "password"; keyPath?: string; + password?: string; } export interface SshExecResult { diff --git a/src/pages/Channels.tsx b/src/pages/Channels.tsx index f87e0c7..17f9fa5 100644 --- a/src/pages/Channels.tsx +++ b/src/pages/Channels.tsx @@ -1,5 +1,5 @@ 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 { useInstance } from "@/lib/instance-context"; import { Card, CardContent } from "@/components/ui/card"; @@ -17,11 +17,6 @@ import { } from "@/components/ui/select"; import { CreateAgentDialog, type CreateAgentResult } from "@/components/CreateAgentDialog"; -interface Binding { - agentId: string; - match: { channel: string; peer?: { id: string; kind: string } }; -} - interface AgentGroup { identity: string; emoji?: string; @@ -91,9 +86,9 @@ export function Channels({ const refreshBindings = useCallback(() => { if (isRemote) { 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 { - 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]); @@ -120,12 +115,15 @@ export function Channels({ refreshBindings(); if (!isRemote) { 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(); refreshDiscordCache(); }, [isRemote, refreshAgents, refreshBindings, refreshChannelNodes, refreshDiscordCache]); const handleRefreshDiscord = () => { + if (isRemote) return; // Refresh is local-only setRefreshing("discord"); api.refreshDiscordGuildChannels() .then((channels) => { @@ -137,6 +135,7 @@ export function Channels({ }; const handleRefreshPlatform = (platform: string) => { + if (isRemote) return; // Refresh is local-only setRefreshing(platform); api.listChannelsMinimal() .then((nodes) => { @@ -250,14 +249,12 @@ export function Channels({ ))} ))} - {!isRemote && ( - <> - - - + New agent... - - - )} + <> + + + + New agent... + + ); @@ -367,36 +364,33 @@ export function Channels({ ))} - {/* Create Agent Dialog — local only */} - {!isRemote && ( - { - setShowCreateAgent(open); - if (!open) setPendingChannel(null); - }} - modelProfiles={modelProfiles} - instanceId={instanceId} - isRemote={isRemote} - onCreated={(result: CreateAgentResult) => { - refreshAgents(); - if (pendingChannel) { - handleAssign(pendingChannel.platform, pendingChannel.peerId, result.agentId); - // Apply persona as systemPrompt on the channel if provided - if (result.persona && pendingChannel.platform === "discord") { - const ch = discordChannels.find((c) => c.channelId === pendingChannel.peerId); - if (ch) { - const patch = JSON.stringify({ - channels: { discord: { guilds: { [ch.guildId]: { channels: { [ch.channelId]: { systemPrompt: result.persona } } } } } }, - }); - api.applyConfigPatch(patch, {}).catch((e) => showToast?.(String(e), "error")); - } + { + setShowCreateAgent(open); + if (!open) setPendingChannel(null); + }} + modelProfiles={modelProfiles} + onCreated={(result: CreateAgentResult) => { + refreshAgents(); + if (pendingChannel) { + handleAssign(pendingChannel.platform, pendingChannel.peerId, result.agentId); + if (result.persona && pendingChannel.platform === "discord") { + const ch = discordChannels.find((c) => c.channelId === pendingChannel.peerId); + if (ch) { + const patch = JSON.stringify({ + channels: { discord: { guilds: { [ch.guildId]: { channels: { [ch.channelId]: { systemPrompt: result.persona } } } } } }, + }); + const patchPromise = isRemote + ? api.remoteApplyConfigPatch(instanceId, patch, {}) + : api.applyConfigPatch(patch, {}); + patchPromise.catch((e) => showToast?.(String(e), "error")); } - setPendingChannel(null); } - }} - /> - )} + setPendingChannel(null); + } + }} + /> ); } diff --git a/src/pages/Doctor.tsx b/src/pages/Doctor.tsx index 7091365..aa1867e 100644 --- a/src/pages/Doctor.tsx +++ b/src/pages/Doctor.tsx @@ -2,7 +2,8 @@ import { useEffect, useMemo, useReducer, useState } from "react"; import { api } from "@/lib/api"; import { useInstance } from "@/lib/instance-context"; 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 { Card, CardHeader, @@ -30,23 +31,10 @@ import { DialogTitle, } 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() { const { instanceId, isRemote, isConnected } = useInstance(); const [state, dispatch] = useReducer(reducer, initialState); const [rawOutput, setRawOutput] = useState(null); - const [memoryFiles, setMemoryFiles] = useState([]); const [sessionFiles, setSessionFiles] = useState([]); const [backups, setBackups] = useState([]); const [dataMessage, setDataMessage] = useState(""); @@ -84,17 +72,14 @@ export function Doctor() { })); }, [sessionFiles]); - const totalMemoryBytes = useMemo( - () => memoryFiles.reduce((sum, f) => sum + f.sizeBytes, 0), - [memoryFiles], - ); const totalSessionBytes = useMemo( () => sessionFiles.reduce((sum, f) => sum + f.sizeBytes, 0), [sessionFiles], ); function runDoctorCmd(): Promise { - if (isRemote && isConnected) { + if (isRemote) { + if (!isConnected) return Promise.reject("Not connected"); return api.remoteRunDoctor(instanceId).then((report) => { // Remote doctor may return a rawOutput field instead of structured issues const raw = (report as unknown as Record).rawOutput; @@ -117,9 +102,6 @@ export function Doctor() { .then(setSessionFiles) .catch(() => setDataMessage("Failed to load remote session files")); } else { - api.listMemoryFiles() - .then(setMemoryFiles) - .catch(() => setDataMessage("Failed to load memory files")); api.listSessionFiles() .then(setSessionFiles) .catch(() => setDataMessage("Failed to load session files")); @@ -156,11 +138,13 @@ export function Doctor() { ); } refreshData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [instanceId, isRemote, isConnected]); useEffect(() => { + if (isRemote) { setBackups([]); return; } api.listBackups().then(setBackups).catch((e) => console.error("Failed to load backups:", e)); - }, []); + }, [instanceId, isRemote]); return (
diff --git a/src/pages/History.tsx b/src/pages/History.tsx index bd1947d..f165810 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -21,10 +21,13 @@ export function History() { const [message, setMessage] = useState(""); const refreshHistory = () => { - const promise = isRemote && isConnected - ? api.remoteListHistory(instanceId) - : api.listHistory(50, 0); - return promise + if (isRemote) { + if (!isConnected) return; + return api.remoteListHistory(instanceId) + .then((resp) => setHistory(resp.items)) + .catch(() => setMessage("Failed to load history")); + } + return api.listHistory(50, 0) .then((resp) => setHistory(resp.items)) .catch(() => setMessage("Failed to load history")); }; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 3e485b3..2038880 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -24,7 +24,7 @@ import { import { CreateAgentDialog } from "@/components/CreateAgentDialog"; import { RecipeCard } from "@/components/RecipeCard"; 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 { useInstance } from "@/lib/instance-context"; @@ -80,13 +80,10 @@ export function Home({ const fetchStatus = useCallback(() => { if (isRemote) { if (!isConnected) return; // Wait for SSH connection - api.remoteGetSystemStatus(instanceId).then((s) => { - const healthy = s.healthy as boolean ?? false; - const activeAgents = s.activeAgents as number ?? 0; - const globalDefaultModel = s.globalDefaultModel as string | undefined; - setStatus({ healthy, activeAgents, globalDefaultModel }); + api.remoteGetSystemStatus(instanceId).then((s: RemoteSystemStatus) => { + setStatus({ healthy: s.healthy, activeAgents: s.activeAgents, globalDefaultModel: s.globalDefaultModel }); 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)); } else { api.getStatusLight().then((s) => { @@ -167,7 +164,7 @@ export function Home({ const timer = setTimeout(() => { if (isRemote) { if (!isConnected) return; - api.remoteCheckOpeclawUpdate(instanceId).then((u) => { + api.remoteCheckOpenclawUpdate(instanceId).then((u) => { setUpdateInfo({ available: u.upgradeAvailable, latest: u.latestVersion ?? undefined }); }).catch((e) => console.error("Failed to check remote update:", e)); } else { @@ -186,6 +183,7 @@ export function Home({ }, [isRemote, isConnected, instanceId]); const handleDeleteAgent = (agentId: string) => { + if (isRemote && !isConnected) return; const deletePromise = isRemote ? api.remoteDeleteAgent(instanceId, agentId) : api.deleteAgent(agentId); @@ -526,8 +524,6 @@ export function Home({ onOpenChange={setShowCreateAgent} modelProfiles={modelProfiles} onCreated={() => refreshAgents()} - instanceId={instanceId} - isRemote={isRemote} /> );