diff --git a/server/studio-settings.js b/server/studio-settings.js index a36ca0c..5e81dea 100644 --- a/server/studio-settings.js +++ b/server/studio-settings.js @@ -58,21 +58,9 @@ const readJsonFile = (filePath) => { const DEFAULT_GATEWAY_URL = "ws://localhost:18789"; const OPENCLAW_CONFIG_FILENAME = "openclaw.json"; -const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]); const isRecord = (value) => Boolean(value && typeof value === "object"); -const isLocalGatewayUrl = (value) => { - const trimmed = typeof value === "string" ? value.trim() : ""; - if (!trimmed) return false; - try { - const parsed = new URL(trimmed); - return LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase()); - } catch { - return false; - } -}; - const readOpenclawGatewayDefaults = (env = process.env) => { try { const stateDir = resolveStateDir(env); @@ -100,7 +88,7 @@ const loadUpstreamGatewaySettings = (env = process.env) => { const gateway = parsed && typeof parsed === "object" ? parsed.gateway : null; const url = typeof gateway?.url === "string" ? gateway.url.trim() : ""; const token = typeof gateway?.token === "string" ? gateway.token.trim() : ""; - if (!token && (!url || isLocalGatewayUrl(url))) { + if (!token) { const defaults = readOpenclawGatewayDefaults(env); if (defaults) { return { diff --git a/src/app/api/gateway/agent-state/route.ts b/src/app/api/gateway/agent-state/route.ts index d57d514..94c77af 100644 --- a/src/app/api/gateway/agent-state/route.ts +++ b/src/app/api/gateway/agent-state/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { restoreAgentStateLocally, trashAgentStateLocally } from "@/lib/agent-state/local"; -import { isLocalGatewayUrl } from "@/lib/gateway/local-gateway"; +import { isLikelyLocalGatewayUrl } from "@/lib/gateway/local-gateway"; import { resolveConfiguredSshTarget, resolveGatewaySshTargetFromGatewayUrl, @@ -30,7 +30,7 @@ const resolveAgentStateSshTarget = (): string | null => { if (configured) return configured; const settings = loadStudioSettings(); const gatewayUrl = settings.gateway?.url ?? ""; - if (isLocalGatewayUrl(gatewayUrl)) return null; + if (isLikelyLocalGatewayUrl(gatewayUrl)) return null; return resolveGatewaySshTargetFromGatewayUrl(gatewayUrl, process.env); }; diff --git a/src/app/api/gateway/media/route.ts b/src/app/api/gateway/media/route.ts index bd48e1c..39ee95a 100644 --- a/src/app/api/gateway/media/route.ts +++ b/src/app/api/gateway/media/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; -import { isLocalGatewayUrl } from "@/lib/gateway/local-gateway"; +import { isLikelyLocalGatewayUrl } from "@/lib/gateway/local-gateway"; import { resolveConfiguredSshTarget, resolveGatewaySshTargetFromGatewayUrl, @@ -151,7 +151,7 @@ PY const resolveSshTarget = (): string | null => { const settings = loadStudioSettings(); const gatewayUrl = settings.gateway?.url ?? ""; - if (isLocalGatewayUrl(gatewayUrl)) return null; + if (isLikelyLocalGatewayUrl(gatewayUrl)) return null; const configured = resolveConfiguredSshTarget(process.env); if (configured) return configured; return resolveGatewaySshTargetFromGatewayUrl(gatewayUrl, process.env); diff --git a/src/app/api/gateway/skills/remove/route.ts b/src/app/api/gateway/skills/remove/route.ts index 0fe047c..bcdd4dc 100644 --- a/src/app/api/gateway/skills/remove/route.ts +++ b/src/app/api/gateway/skills/remove/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; -import { isLocalGatewayUrl } from "@/lib/gateway/local-gateway"; +import { isLikelyLocalGatewayUrl } from "@/lib/gateway/local-gateway"; import { removeSkillLocally } from "@/lib/skills/remove-local"; import type { RemovableSkillSource, SkillRemoveRequest } from "@/lib/skills/types"; import { @@ -33,7 +33,7 @@ const resolveSkillRemovalSshTarget = (): string | null => { if (configured) return configured; const settings = loadStudioSettings(); const gatewayUrl = settings.gateway?.url ?? ""; - if (isLocalGatewayUrl(gatewayUrl)) return null; + if (isLikelyLocalGatewayUrl(gatewayUrl)) return null; return resolveGatewaySshTargetFromGatewayUrl(gatewayUrl, process.env); }; diff --git a/src/features/agents/components/AgentAvatarEditorPanel.tsx b/src/features/agents/components/AgentAvatarEditorPanel.tsx index f75d491..f1f0230 100644 --- a/src/features/agents/components/AgentAvatarEditorPanel.tsx +++ b/src/features/agents/components/AgentAvatarEditorPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { RefreshCcw, Shuffle } from "lucide-react"; import { AGENT_AVATAR_BOTTOM_STYLE_OPTIONS, @@ -22,8 +22,16 @@ export type AgentAvatarEditorPanelProps = { agentName: string; initialProfile: AgentAvatarProfile | null | undefined; onSave: (profile: AgentAvatarProfile) => Promise | void; + onDraftChange?: (profile: AgentAvatarProfile) => void; onCancel?: () => void; onSaved?: () => void; + cancelLabel?: string; + saveLabel?: string; + showActions?: boolean; +}; + +export type AgentAvatarEditorPanelHandle = { + save: () => Promise; }; const pillClassName = @@ -32,14 +40,24 @@ const pillClassName = const colorSwatchClassName = "h-7 w-7 rounded-full border-2 transition-transform hover:scale-105"; -export const AgentAvatarEditorPanel = ({ - agentId, - agentName, - initialProfile, - onSave, - onCancel, - onSaved, -}: AgentAvatarEditorPanelProps) => { +export const AgentAvatarEditorPanel = forwardRef< + AgentAvatarEditorPanelHandle, + AgentAvatarEditorPanelProps +>(function AgentAvatarEditorPanel( + { + agentId, + agentName, + initialProfile, + onSave, + onDraftChange, + onCancel, + onSaved, + cancelLabel = "Cancel", + saveLabel = "Save avatar", + showActions = true, + }: AgentAvatarEditorPanelProps, + ref +) { const fallbackProfile = useMemo( () => createDefaultAgentAvatarProfile(agentId), [agentId] @@ -52,7 +70,11 @@ export const AgentAvatarEditorPanel = ({ setDraft(resolvedInitialProfile); }, [resolvedInitialProfile]); - const save = async () => { + useEffect(() => { + onDraftChange?.(draft); + }, [draft, onDraftChange]); + + const save = useCallback(async () => { if (saving) return; setSaving(true); try { @@ -61,7 +83,15 @@ export const AgentAvatarEditorPanel = ({ } finally { setSaving(false); } - }; + }, [draft, onSave, onSaved, saving]); + + useImperativeHandle( + ref, + () => ({ + save, + }), + [save] + ); return (
@@ -99,26 +129,28 @@ export const AgentAvatarEditorPanel = ({
-
- - -
+ {showActions ? ( +
+ + +
+ ) : null}

@@ -420,4 +452,4 @@ export const AgentAvatarEditorPanel = ({

); -}; +}); diff --git a/src/features/agents/components/AgentAvatarPreview3D.tsx b/src/features/agents/components/AgentAvatarPreview3D.tsx index 70727d7..d575302 100644 --- a/src/features/agents/components/AgentAvatarPreview3D.tsx +++ b/src/features/agents/components/AgentAvatarPreview3D.tsx @@ -305,11 +305,9 @@ export const AgentAvatarPreview3D = ({ () => profile ?? createDefaultAgentAvatarProfile("preview"), [profile] ); - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - setIsReady(false); - }, [resolvedProfile]); + const profileKey = useMemo(() => JSON.stringify(resolvedProfile), [resolvedProfile]); + const [readyProfileKey, setReadyProfileKey] = useState(null); + const isReady = readyProfileKey === profileKey; return (
@@ -321,7 +319,7 @@ export const AgentAvatarPreview3D = ({
) : null} - + @@ -329,7 +327,7 @@ export const AgentAvatarPreview3D = ({ { - setIsReady(true); + setReadyProfileKey(profileKey); }} /> diff --git a/src/features/agents/components/AgentChatPanel.tsx b/src/features/agents/components/AgentChatPanel.tsx index 7a60afe..e280e35 100644 --- a/src/features/agents/components/AgentChatPanel.tsx +++ b/src/features/agents/components/AgentChatPanel.tsx @@ -1100,8 +1100,8 @@ const AgentChatComposer = memo(function AgentChatComposer({
@@ -1122,7 +1122,7 @@ const AgentChatComposer = memo(function AgentChatComposer({