From 1297c12a730456bca24a1e6e4c3d5dfb4ff66898 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 4 Feb 2026 21:44:21 +0530 Subject: [PATCH] feat(agents): Refactor gateway configuration handling and enhance UI for agent creation and editing --- backend/app/api/gateway.py | 52 +- .../src/app/agents/[agentId]/edit/page.tsx | 234 +++++---- frontend/src/app/agents/new/page.tsx | 234 +++++---- .../src/app/boards/[boardId]/edit/page.tsx | 465 +++++++++++++----- frontend/src/app/boards/new/page.tsx | 454 ++++++++++++----- 5 files changed, 968 insertions(+), 471 deletions(-) diff --git a/backend/app/api/gateway.py b/backend/app/api/gateway.py index cf127620..380c5a81 100644 --- a/backend/app/api/gateway.py +++ b/backend/app/api/gateway.py @@ -24,11 +24,19 @@ from app.models.boards import Board router = APIRouter(prefix="/gateway", tags=["gateway"]) -def _require_board_config(session: Session, board_id: str | None) -> tuple[Board, GatewayConfig]: +def _resolve_gateway_config( + session: Session, + board_id: str | None, + gateway_url: str | None, + gateway_token: str | None, + gateway_main_session_key: str | None, +) -> tuple[Board | None, GatewayConfig, str | None]: + if gateway_url: + return None, GatewayConfig(url=gateway_url, token=gateway_token), gateway_main_session_key if not board_id: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="board_id is required", + detail="board_id or gateway_url is required", ) board = session.get(Board, board_id) if board is None: @@ -38,28 +46,31 @@ def _require_board_config(session: Session, board_id: str | None) -> tuple[Board status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Board gateway_url is required", ) - if not board.gateway_main_session_key: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Board gateway_main_session_key is required", - ) - return board, GatewayConfig(url=board.gateway_url, token=board.gateway_token) + return board, GatewayConfig(url=board.gateway_url, token=board.gateway_token), board.gateway_main_session_key @router.get("/status") async def gateway_status( board_id: str | None = Query(default=None), + gateway_url: str | None = Query(default=None), + gateway_token: str | None = Query(default=None), + gateway_main_session_key: str | None = Query(default=None), session: Session = Depends(get_session), auth: AuthContext = Depends(require_admin_auth), ) -> dict[str, object]: - board, config = _require_board_config(session, board_id) + board, config, main_session = _resolve_gateway_config( + session, + board_id, + gateway_url, + gateway_token, + gateway_main_session_key, + ) try: sessions = await openclaw_call("sessions.list", config=config) if isinstance(sessions, dict): sessions_list = list(sessions.get("sessions") or []) else: sessions_list = list(sessions or []) - main_session = board.gateway_main_session_key main_session_entry: object | None = None main_session_error: str | None = None if main_session: @@ -73,7 +84,7 @@ async def gateway_status( main_session_error = str(exc) return { "connected": True, - "gateway_url": board.gateway_url, + "gateway_url": config.url, "sessions_count": len(sessions_list), "sessions": sessions_list, "main_session_key": main_session, @@ -83,7 +94,7 @@ async def gateway_status( except OpenClawGatewayError as exc: return { "connected": False, - "gateway_url": board.gateway_url, + "gateway_url": config.url, "error": str(exc), } @@ -94,7 +105,13 @@ async def list_sessions( session: Session = Depends(get_session), auth: AuthContext = Depends(require_admin_auth), ) -> dict[str, object]: - board, config = _require_board_config(session, board_id) + board, config, main_session = _resolve_gateway_config( + session, + board_id, + None, + None, + None, + ) try: sessions = await openclaw_call("sessions.list", config=config) except OpenClawGatewayError as exc: @@ -104,7 +121,6 @@ async def list_sessions( else: sessions_list = list(sessions or []) - main_session = board.gateway_main_session_key main_session_entry: object | None = None if main_session: try: @@ -130,7 +146,13 @@ async def get_gateway_session( session: Session = Depends(get_session), auth: AuthContext = Depends(require_admin_auth), ) -> dict[str, object]: - board, config = _require_board_config(session, board_id) + board, config, main_session = _resolve_gateway_config( + session, + board_id, + None, + None, + None, + ) try: sessions = await openclaw_call("sessions.list", config=config) except OpenClawGatewayError as exc: diff --git a/frontend/src/app/agents/[agentId]/edit/page.tsx b/frontend/src/app/agents/[agentId]/edit/page.tsx index 81e18bd3..6b5a3e65 100644 --- a/frontend/src/app/agents/[agentId]/edit/page.tsx +++ b/frontend/src/app/agents/[agentId]/edit/page.tsx @@ -162,114 +162,146 @@ export default function EditAgentPage() { return ( -
-

Sign in to edit agents.

- - - +
+
+

Sign in to edit agents.

+ + + +
-
-
-

- Edit agent -

-

- {agent?.name ?? "Agent"} -

-

- Status is controlled by agent heartbeat. -

-
-
-
- - setName(event.target.value)} - placeholder="e.g. Deploy bot" - disabled={isLoading} - /> -
-
- - - {boards.length === 0 ? ( -

- Create a board before assigning agents. -

- ) : null} -
-
- - setHeartbeatEvery(event.target.value)} - placeholder="e.g. 10m" - disabled={isLoading} - /> -

- Set how often this agent runs HEARTBEAT.md. +

+
+
+

+ {agent?.name ?? "Edit agent"} +

+

+ Status is controlled by agent heartbeat.

-
- - -
- {error ? ( -
- {error} +
+ +
+ +
+

+ Agent identity +

+
+
+ + setName(event.target.value)} + placeholder="e.g. Deploy bot" + disabled={isLoading} + /> +
+
+ + + {boards.length === 0 ? ( +

+ Create a board before assigning agents. +

+ ) : null} +
+
- ) : null} - - - -
+ +
+

+ Heartbeat settings +

+
+
+ + setHeartbeatEvery(event.target.value)} + placeholder="e.g. 10m" + disabled={isLoading} + /> +

+ Set how often this agent runs HEARTBEAT.md. +

+
+
+ + +
+
+
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+ + +
+ +
+
); diff --git a/frontend/src/app/agents/new/page.tsx b/frontend/src/app/agents/new/page.tsx index 00bf1679..45ef3819 100644 --- a/frontend/src/app/agents/new/page.tsx +++ b/frontend/src/app/agents/new/page.tsx @@ -114,114 +114,146 @@ export default function NewAgentPage() { return ( -
-

Sign in to create an agent.

- - - +
+
+

Sign in to create an agent.

+ + + +
-
-
-

- New agent -

-

- Register an agent. -

-

- Agents start in provisioning until they check in. -

-
-
-
- - setName(event.target.value)} - placeholder="e.g. Deploy bot" - disabled={isLoading} - /> -
-
- - - {boards.length === 0 ? ( -

- Create a board before adding agents. -

- ) : null} -
-
- - setHeartbeatEvery(event.target.value)} - placeholder="e.g. 10m" - disabled={isLoading} - /> -

- Set how often this agent runs HEARTBEAT.md (e.g. 10m, 30m, 2h). +

+
+
+

+ Create agent +

+

+ Agents start in provisioning until they check in.

-
- - -
- {error ? ( -
- {error} +
+ +
+ +
+

+ Agent identity +

+
+
+ + setName(event.target.value)} + placeholder="e.g. Deploy bot" + disabled={isLoading} + /> +
+
+ + + {boards.length === 0 ? ( +

+ Create a board before adding agents. +

+ ) : null} +
+
- ) : null} - - - -
+ +
+

+ Heartbeat settings +

+
+
+ + setHeartbeatEvery(event.target.value)} + placeholder="e.g. 10m" + disabled={isLoading} + /> +

+ How often this agent runs HEARTBEAT.md (10m, 30m, 2h). +

+
+
+ + +
+
+
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+ + +
+ +
+
); diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index 6196aa4c..3c803b97 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -11,9 +11,78 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { getApiBaseUrl } from "@/lib/api-base"; +import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react"; const apiBase = getApiBaseUrl(); +const DEFAULT_IDENTITY_TEMPLATE = `# IDENTITY.md + +Name: {{ agent_name }} + +Agent ID: {{ agent_id }} + +Creature: AI + +Vibe: calm, precise, helpful + +Emoji: :gear: +`; + +const DEFAULT_SOUL_TEMPLATE = `# SOUL.md + +_You're not a chatbot. You're becoming someone._ + +## Core Truths + +**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" -- just help. Actions speak louder than filler words. + +**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps. + +**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions. + +**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning). + +**Remember you're a guest.** You have access to someone's life -- their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect. + +## Boundaries + +- Private things stay private. Period. +- When in doubt, ask before acting externally. +- Never send half-baked replies to messaging surfaces. +- You're not the user's voice -- be careful in group chats. + +## Vibe + +Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good. + +## Continuity + +Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist. + +If you change this file, tell the user -- it's your soul, and they should know. + +--- + +_This file is yours to evolve. As you learn who you are, update it._ +`; + +const validateGatewayUrl = (value: string) => { + const trimmed = value.trim(); + if (!trimmed) return "Gateway URL is required."; + try { + const url = new URL(trimmed); + if (url.protocol !== "ws:" && url.protocol !== "wss:") { + return "Gateway URL must start with ws:// or wss://."; + } + if (!url.port) { + return "Gateway URL must include an explicit port."; + } + return null; + } catch { + return "Enter a valid gateway URL including port."; + } +}; + type Board = { id: string; name: string; @@ -41,15 +110,69 @@ export default function EditBoardPage() { const [board, setBoard] = useState(null); const [name, setName] = useState(""); - const [slug, setSlug] = useState(""); const [gatewayUrl, setGatewayUrl] = useState(""); const [gatewayToken, setGatewayToken] = useState(""); const [gatewayMainSessionKey, setGatewayMainSessionKey] = useState(""); const [gatewayWorkspaceRoot, setGatewayWorkspaceRoot] = useState(""); - const [identityTemplate, setIdentityTemplate] = useState(""); - const [soulTemplate, setSoulTemplate] = useState(""); + const [identityTemplate, setIdentityTemplate] = useState( + DEFAULT_IDENTITY_TEMPLATE + ); + const [soulTemplate, setSoulTemplate] = useState(DEFAULT_SOUL_TEMPLATE); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const [gatewayUrlError, setGatewayUrlError] = useState(null); + const [gatewayCheckStatus, setGatewayCheckStatus] = useState< + "idle" | "checking" | "success" | "error" + >("idle"); + const [gatewayCheckMessage, setGatewayCheckMessage] = useState( + null + ); + + const runGatewayCheck = async () => { + const validationError = validateGatewayUrl(gatewayUrl); + setGatewayUrlError(validationError); + if (validationError) { + setGatewayCheckStatus("error"); + setGatewayCheckMessage(validationError); + return; + } + if (!isSignedIn) return; + setGatewayCheckStatus("checking"); + setGatewayCheckMessage(null); + try { + const token = await getToken(); + const params = new URLSearchParams({ + gateway_url: gatewayUrl.trim(), + }); + if (gatewayToken.trim()) { + params.set("gateway_token", gatewayToken.trim()); + } + if (gatewayMainSessionKey.trim()) { + params.set("gateway_main_session_key", gatewayMainSessionKey.trim()); + } + const response = await fetch( + `${apiBase}/api/v1/gateway/status?${params.toString()}`, + { + headers: { + Authorization: token ? `Bearer ${token}` : "", + }, + }, + ); + const data = await response.json(); + if (!response.ok || !data?.connected) { + setGatewayCheckStatus("error"); + setGatewayCheckMessage(data?.error ?? "Unable to reach gateway."); + return; + } + setGatewayCheckStatus("success"); + setGatewayCheckMessage("Gateway reachable."); + } catch (err) { + setGatewayCheckStatus("error"); + setGatewayCheckMessage( + err instanceof Error ? err.message : "Unable to reach gateway." + ); + } + }; const loadBoard = async () => { if (!isSignedIn || !boardId) return; @@ -66,12 +189,11 @@ export default function EditBoardPage() { const data = (await response.json()) as Board; setBoard(data); setName(data.name); - setSlug(data.slug); setGatewayUrl(data.gateway_url ?? ""); - setGatewayMainSessionKey(data.gateway_main_session_key ?? ""); - setGatewayWorkspaceRoot(data.gateway_workspace_root ?? ""); - setIdentityTemplate(data.identity_template ?? ""); - setSoulTemplate(data.soul_template ?? ""); + setGatewayMainSessionKey(data.gateway_main_session_key ?? "agent:main:main"); + setGatewayWorkspaceRoot(data.gateway_workspace_root ?? "~/.openclaw"); + setIdentityTemplate(data.identity_template ?? DEFAULT_IDENTITY_TEMPLATE); + setSoulTemplate(data.soul_template ?? DEFAULT_SOUL_TEMPLATE); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong."); } finally { @@ -92,13 +214,20 @@ export default function EditBoardPage() { setError("Board name is required."); return; } + const gatewayValidation = validateGatewayUrl(gatewayUrl); + setGatewayUrlError(gatewayValidation); + if (gatewayValidation) { + setGatewayCheckStatus("error"); + setGatewayCheckMessage(gatewayValidation); + return; + } setIsLoading(true); setError(null); try { const token = await getToken(); const payload: Partial & { gateway_token?: string | null } = { name: trimmed, - slug: slug.trim() || slugify(trimmed), + slug: board?.slug ?? slugify(trimmed), gateway_url: gatewayUrl.trim() || null, gateway_main_session_key: gatewayMainSessionKey.trim() || null, gateway_workspace_root: gatewayWorkspaceRoot.trim() || null, @@ -130,133 +259,209 @@ export default function EditBoardPage() { return ( -
-

Sign in to edit boards.

- - - +
+
+

Sign in to edit boards.

+ + + +
-
-
-

- Edit board -

-

- {board?.name ?? "Board"} -

-

- Update the board identity and gateway connection. -

+
+
+
+

+ {board?.name ?? "Edit board"} +

+

+ Update the board identity and gateway connection. +

+
-
-
- - setName(event.target.value)} - placeholder="e.g. Product ops" - disabled={isLoading} - /> -
-
- - setSlug(event.target.value)} - placeholder="product-ops" - disabled={isLoading} - /> -
-
- - setGatewayUrl(event.target.value)} - placeholder="ws://gateway:18789" - disabled={isLoading} - /> -
-
- - setGatewayToken(event.target.value)} - placeholder="Leave blank to keep current token" - disabled={isLoading} - /> -
-
- - setGatewayMainSessionKey(event.target.value)} - placeholder="agent:main:main" - disabled={isLoading} - /> -
-
- - setGatewayWorkspaceRoot(event.target.value)} - placeholder="~/.openclaw" - disabled={isLoading} - /> -
-
- -