From f98c56ab7e03c9fef35ef10dbfd9f5720e067530 Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 18 Apr 2026 10:26:51 -0700 Subject: [PATCH] =?UTF-8?q?[hAIk]:=20Holy=20fucking=20shit=20the=20agent?= =?UTF-8?q?=20finally=20works=20in=20the=20frontend:=20align=20frontend/ba?= =?UTF-8?q?ckend=20naming:=20rename=20AgentSession.id=E2=86=92session=5Fid?= =?UTF-8?q?=20across=20agentsTypes,=20agentsReducers,=20agentsExtraReducer?= =?UTF-8?q?s,=20AgentCard,=20BrowserAgentOverlay,=20DashboardCanvas,=20use?= =?UTF-8?q?AgentChat,=20useToolbarActions,=20useDashboardKeyboard,=20Messa?= =?UTF-8?q?geActions,=20and=20backend-bridge=20agents/dashboards;=20rename?= =?UTF-8?q?=20CardPosition.zOrder=E2=86=92z=5Forder=20in=20dashboardLayout?= =?UTF-8?q?Types,=20dashboardLayoutReducers,=20dashboardLayoutSlice,=20and?= =?UTF-8?q?=20DashboardCanvas;=20fix=20circular=20import=20between=20dashb?= =?UTF-8?q?oards.py=20and=20agents.py=20by=20deferring=20imports;=20add=20?= =?UTF-8?q?NINE=5FROUTER=5FMODEL=5FMAP=20for=20sonnet/opus/haiku=20alias?= =?UTF-8?q?=20resolution=20in=20launch=5Fagent;=20make=20build=5Fagent=5Ft?= =?UTF-8?q?oolkit=20async=20and=20pass=20load=5Fdashboard/save=5Fdashboard?= =?UTF-8?q?=20into=20browser=20delegation=20toolkit;=20fix=20PromptMsgDict?= =?UTF-8?q?=20to=20use=20TypedDict,=20fix=20zip=20type=20hint=20bug,=20and?= =?UTF-8?q?=20fix=20browser=20agent=20model=20reference=20(parent.model?= =?UTF-8?q?=E2=86=92parent.config.model);=20add=20p=5Fon=5Ftask=5Fdone=20c?= =?UTF-8?q?allback=20with=20traceback=20logging=20on=20Agent,=20capture=20?= =?UTF-8?q?stderr=20and=20full=20tracebacks=20in=20run=5Fagent=5Floop;=20a?= =?UTF-8?q?dd=20unwrages=20helper=20in=20agentsExtraReducers=20to=20normal?= =?UTF-8?q?ize=20nested=20message=20payloads;=20pipe=20frontend=20console-?= =?UTF-8?q?message=20events=20to=20Electron=20stdout;=20enable=20ws:true?= =?UTF-8?q?=20on=20Vite=20proxy;=20add=20diagnostic=20console.logs=20acros?= =?UTF-8?q?s=20WS=20handlers,=20session=20lifecycle,=20and=20dashboard=20u?= =?UTF-8?q?pdates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agents/agent_utils/build_agent_toolkit.py | 13 ++- backend/apps/agents/agents.py | 17 ++-- backend/apps/dashboards/dashboards.py | 5 +- backend/apps/tools/tools.py | 2 +- backend/core/Agent/Agent.py | 17 +++- .../Agent/run_agent_loop/run_agent_loop.py | 26 +++++- .../shared_structs/agent/Message/Message.py | 18 ++--- .../core/shared_structs/card/CardPosition.py | 5 +- .../make_builtin_toolkit.py | 13 ++- .../make_create_browser_agent_handler.py | 24 +++++- .../handlers/utils/run_browser_agent.py | 2 +- .../make_browser_delegation_toolkit.py | 15 +++- electron/main.js | 6 ++ .../app/pages/AgentChat/hooks/useAgentChat.ts | 2 +- .../pages/AgentChat/thread/MessageActions.tsx | 4 +- .../src/app/pages/Dashboard/AgentCard.tsx | 46 +++++------ .../pages/Dashboard/BrowserAgentOverlay.tsx | 12 +-- .../app/pages/Dashboard/DashboardCanvas.tsx | 18 ++--- .../Dashboard/hooks/useDashboardKeyboard.ts | 2 +- .../Dashboard/hooks/useToolbarActions.ts | 4 +- .../src/shared/backend-bridge/apps/agents.ts | 5 +- .../shared/backend-bridge/apps/dashboards.ts | 9 ++- .../src/shared/state/agentsExtraReducers.ts | 80 +++++++++++-------- frontend/src/shared/state/agentsReducers.ts | 20 +++-- frontend/src/shared/state/agentsTypes.ts | 2 +- .../shared/state/dashboardLayoutReducers.ts | 20 ++--- .../src/shared/state/dashboardLayoutSlice.ts | 18 +++-- .../src/shared/state/dashboardLayoutTypes.ts | 6 +- frontend/src/shared/ws/wsEventHandlers.ts | 10 ++- frontend/vite.config.ts | 1 + 30 files changed, 281 insertions(+), 141 deletions(-) diff --git a/backend/apps/agents/agent_utils/build_agent_toolkit.py b/backend/apps/agents/agent_utils/build_agent_toolkit.py index 6dbd2ca8..e1ab9da4 100644 --- a/backend/apps/agents/agent_utils/build_agent_toolkit.py +++ b/backend/apps/agents/agent_utils/build_agent_toolkit.py @@ -3,6 +3,7 @@ from backend.core.tools.shared_structs.Toolkit import Toolkit from backend.core.tools.make_builtin_toolkit.make_builtin_toolkit import make_builtin_toolkit from backend.apps.agents.COMMS_MANAGER.COMMS_MANAGER import CommsManager from backend.apps.tools.tools import load_user_toolkit, load_builtin_permissions +from backend.apps.dashboards.dashboards import DASHBOARD_STORE from typing import Dict, Optional from typeguard import typechecked from backend.core.tools.shared_structs.TOOL_PERMISSIONS import TOOL_PERMISSIONS @@ -23,13 +24,19 @@ def p_apply_builtin_permission_overrides(toolkit: Toolkit, permissions: dict[str @typechecked -def build_agent_toolkit(agent: Agent, sessions: Dict[str, Agent], comms_manager: CommsManager) -> Toolkit: +async def build_agent_toolkit(agent: Agent, sessions: Dict[str, Agent], comms_manager: CommsManager) -> Toolkit: """Build the full toolkit tree: builtin tools + user-installed MCP tools. Also applies saved builtin permission overrides. """ - builtin_toolkit: Toolkit = make_builtin_toolkit(agent, sessions, comms_manager.send_browser_command) - user_toolkit: Optional[Toolkit] = load_user_toolkit() + builtin_toolkit: Toolkit = make_builtin_toolkit( + parent=agent, + agent_registry=sessions, + send_browser_command=comms_manager.send_browser_command, + load_dashboard=DASHBOARD_STORE.load, + save_dashboard=DASHBOARD_STORE.save, + ) + user_toolkit: Optional[Toolkit] = await load_user_toolkit() full_toolkit: Optional[Toolkit] = None if user_toolkit is not None: diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index cd9088a1..b846b5b8 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -64,7 +64,7 @@ async def agents_lifespan(): try: stored.status = "stopped" stored.on_event = COMMS_MANAGER.make_session_emitter(stored.session_id) - stored.toolkit = build_agent_toolkit( + stored.toolkit = await build_agent_toolkit( agent=stored, sessions=SESSIONS, comms_manager=COMMS_MANAGER, @@ -144,7 +144,7 @@ async def launch_agent( agent.on_event = COMMS_MANAGER.make_session_emitter(agent.session_id) SESSIONS[agent.session_id] = agent - toolkit: Toolkit = build_agent_toolkit( + toolkit: Toolkit = await build_agent_toolkit( agent=agent, sessions=SESSIONS, comms_manager=COMMS_MANAGER, @@ -166,9 +166,16 @@ async def launch_agent( nine_router_port=NINE_ROUTER_PORT if not settings.anthropic_api_key else None, ) + NINE_ROUTER_MODEL_MAP = { + "sonnet": "cc/claude-sonnet-4-6", + "opus": "cc/claude-opus-4-6", + "haiku": "cc/claude-haiku-4-5-20251001", + } + resolved_model = NINE_ROUTER_MODEL_MAP.get(model, f"cc/{model}") if not settings.anthropic_api_key else model + agent.config = ClaudeAgentOptions( env=env, - model=model, + model=resolved_model, system_prompt=resolved_mode_config.system_prompt, max_turns=max_turns, cwd=resolved_mode_config.cwd, @@ -365,7 +372,7 @@ async def resume_session(session_id: str = Body()) -> dict: raise HTTPException(status_code=404, detail="Session not found in history") agent.status = "stopped" agent.on_event = COMMS_MANAGER.make_session_emitter(agent.session_id) - agent.toolkit = build_agent_toolkit( + agent.toolkit = await build_agent_toolkit( agent=agent, sessions=SESSIONS, comms_manager=COMMS_MANAGER, @@ -395,7 +402,7 @@ async def duplicate_session(session_id: str = Body()) -> dict: clone.pending_approvals = [] clone.sub_agents = [] clone.on_event = COMMS_MANAGER.make_session_emitter(clone.session_id) - clone.toolkit = build_agent_toolkit( + clone.toolkit = await build_agent_toolkit( agent=clone, sessions=SESSIONS, comms_manager=COMMS_MANAGER, diff --git a/backend/apps/dashboards/dashboards.py b/backend/apps/dashboards/dashboards.py index adf6e6e9..eb643609 100644 --- a/backend/apps/dashboards/dashboards.py +++ b/backend/apps/dashboards/dashboards.py @@ -9,7 +9,6 @@ from backend.config.Apps import SubApp from backend.core.db.PydanticStore import PydanticStore from backend.core.shared_structs.dashboard.Dashboard import Dashboard from backend.core.shared_structs.dashboard.DashboardLayout import DashboardLayout -from backend.apps.agents.agents import get_all_sessions, delete_session from backend.apps.settings.settings import load_settings from backend.apps.dashboards.generate_dashboard_name import generate_dashboard_name from backend.ports import NINE_ROUTER_PORT @@ -65,6 +64,8 @@ async def create_dashboard(body: DashboardCreate): # TODO: Maybe parse the output of get_all_sessions into actual Agent objects? @dashboards.router.post("/{dashboard_id}/generate-name") async def generate_name(dashboard_id: str): + # TODO: Fix nested import by passing context as input maybe or idk maybe smthn else + from backend.apps.agents.agents import get_all_sessions dashboard = DASHBOARD_STORE.load(dashboard_id) if not dashboard.auto_named and dashboard.name != "Untitled Dashboard": @@ -133,6 +134,8 @@ async def update_dashboard(dashboard_id: str, body: DashboardUpdate): @dashboards.router.delete("/{dashboard_id}") async def delete_dashboard(dashboard_id: str): + # TODO: Fix nested import somehow idek tho + from backend.apps.agents.agents import get_all_sessions, delete_session DASHBOARD_STORE.load(dashboard_id) # confirm it exists (raises 404 if not) sessions_resp = await get_all_sessions(dashboard_id=dashboard_id) diff --git a/backend/apps/tools/tools.py b/backend/apps/tools/tools.py index acea3779..a96879ad 100644 --- a/backend/apps/tools/tools.py +++ b/backend/apps/tools/tools.py @@ -62,7 +62,7 @@ async def list_builtin_tools() -> dict: return {"tools": BUILTIN_TOOLS} -def load_builtin_permissions() -> dict[str, str]: +def load_builtin_permissions() -> dict[str, TOOL_PERMISSIONS]: if not os.path.exists(BUILTIN_PERMS_PATH): return {} with open(BUILTIN_PERMS_PATH) as f: diff --git a/backend/core/Agent/Agent.py b/backend/core/Agent/Agent.py index 4f60a7a4..50a5d4bd 100644 --- a/backend/core/Agent/Agent.py +++ b/backend/core/Agent/Agent.py @@ -1,5 +1,6 @@ import asyncio import os +import traceback from copy import deepcopy from typing import Any, Dict, List, Literal, Optional from uuid import uuid4 @@ -9,7 +10,7 @@ from pydantic import BaseModel, Field, InstanceOf from typeguard import typechecked from backend.core.Agent.run_agent_loop.run_agent_loop import run_agent_loop -from backend.core.shared_structs.agent.Message.Message import Message +from backend.core.shared_structs.agent.Message.Message import UserMessage from backend.core.shared_structs.agent.ApprovalRequest import ApprovalRequest from backend.core.shared_structs.agent.MessageLog import MessageLog from backend.core.events.events import ( @@ -94,7 +95,7 @@ class Agent(BaseModel): session_id=self.session_id, status="waiting_approval", )) - future: InstanceOf[asyncio.Future] = asyncio.get_event_loop().create_future() + future: asyncio.Future = asyncio.get_event_loop().create_future() try: await self.emit(ApprovalRequestEvent( session_id=self.session_id, @@ -120,7 +121,7 @@ class Agent(BaseModel): return decision @typechecked - async def send_message(self, msg: Message) -> None: + async def send_message(self, msg: UserMessage) -> None: async with self.lock: if self.task is not None and not self.task.done(): debug(f"[Agent.send_message] Agent {self.session_id} is already running") @@ -144,6 +145,16 @@ class Agent(BaseModel): branch_id=self.branch_id, emit=self._handle_event, )) + self.task.add_done_callback(self.p_on_task_done) + + @typechecked + def p_on_task_done(self, task: asyncio.Task) -> None: + if task.cancelled(): + return + exc = task.exception() + if exc: + tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + debug(f"[Agent] Task for {self.session_id} failed:\n{tb}") @typechecked async def stop_agent(self): diff --git a/backend/core/Agent/run_agent_loop/run_agent_loop.py b/backend/core/Agent/run_agent_loop/run_agent_loop.py index 6355257b..8668925d 100644 --- a/backend/core/Agent/run_agent_loop/run_agent_loop.py +++ b/backend/core/Agent/run_agent_loop/run_agent_loop.py @@ -1,4 +1,5 @@ import asyncio +import traceback from swarm_debug import debug from typeguard import typechecked @@ -34,6 +35,17 @@ async def run_agent_loop( assistant / tool_call message into `messages`. """ + stderr_lines: List[str] = [] + original_stderr = options.stderr + + def capture_stderr(line: str) -> None: + stderr_lines.append(line) + debug(f"[claude-cli stderr] {line}") + if original_stderr: + original_stderr(line) + + options.stderr = capture_stderr + async def prompt_stream(): yield prompt_msg @@ -41,10 +53,17 @@ async def run_agent_loop( stream_tool_msg_ids_ordered: List[str] = [] stream_block_index_map: Dict[int, str] = {} + debug(f"Starting query for agent {session_id}", + f"model={options.model}", + f"env_keys={list(options.env.keys()) if options.env else 'none'}", + f"permission_mode={options.permission_mode}", + f"max_turns={options.max_turns}") + try: async for message in query(prompt=prompt_stream(), options=options): if isinstance(message, StreamEvent): + debug(f"[agent {session_id}] stream: {message.event.get('type', '?')}") stream_text_msg_id = await handle_stream_event( session_id=session_id, event=message.event, @@ -55,6 +74,8 @@ async def run_agent_loop( ) elif isinstance(message, AssistantMessage): + content_preview = str(message.content)[:200] + debug(f"[agent {session_id}] assistant: {content_preview}") stream_text_msg_id, stream_tool_msg_ids_ordered, stream_block_index_map, created = ( await handle_assistant_message( session_id=session_id, @@ -68,6 +89,7 @@ async def run_agent_loop( for msg in created: messages.append(msg) + debug(f"[agent {session_id}] completed successfully") if emit: await emit(AgentStatusEvent(session_id=session_id, status="completed")) @@ -77,7 +99,9 @@ async def run_agent_loop( raise except Exception as e: - debug(f"Agent {session_id} error: {e}") + tb = "".join(traceback.format_exception(type(e), e, e.__traceback__)) + stderr_output = "\n".join(stderr_lines) if stderr_lines else "(no stderr captured)" + debug(f"Agent {session_id} error:\n{tb}\n--- CLI stderr ---\n{stderr_output}") error_msg = SystemMessage( content=f"Error: {e}", branch_id=branch_id, diff --git a/backend/core/shared_structs/agent/Message/Message.py b/backend/core/shared_structs/agent/Message/Message.py index d75e9cd1..04f5ae23 100644 --- a/backend/core/shared_structs/agent/Message/Message.py +++ b/backend/core/shared_structs/agent/Message/Message.py @@ -1,6 +1,6 @@ # Message.py -from typing import List, Literal, Dict, Annotated, Union +from typing import List, Literal, Annotated, Union, TypedDict from pydantic import BaseModel, Field from datetime import datetime from uuid import uuid4 @@ -23,13 +23,13 @@ class Message(BaseModel): hidden: bool = False -PromptMsgDict = Dict[ - Literal["type", "message"], - Dict[ - Literal["role", "content"], - List[PromptBlock] - ] - ] +class PromptMessageContent(TypedDict): + role: str + content: List[PromptBlock] + +class PromptMsgDict(TypedDict): + type: str + message: PromptMessageContent class UserMessage(Message): role: Literal["user"] = "user" @@ -51,7 +51,7 @@ class UserMessage(Message): full_text: str = "\n\n".join(p for p in parts if p) blocks: List[PromptBlock] = [TextPromptBlock(type="text", text=full_text)] - for data, media_type in zip[tuple[str, str]](self.images, self.image_media_types): + for data, media_type in zip(self.images, self.image_media_types): blocks.append(ImagePromptBlock( type="image", source=ImageSource(type="base64", media_type=media_type, data=data), diff --git a/backend/core/shared_structs/card/CardPosition.py b/backend/core/shared_structs/card/CardPosition.py index e3163af3..00f0e25c 100644 --- a/backend/core/shared_structs/card/CardPosition.py +++ b/backend/core/shared_structs/card/CardPosition.py @@ -2,8 +2,9 @@ from pydantic import BaseModel class CardPosition(BaseModel): - id: str + session_id: str x: float = 0 y: float = 0 width: float = 420 - height: float = 280 \ No newline at end of file + height: float = 280 + z_order: int = 0 \ No newline at end of file diff --git a/backend/core/tools/make_builtin_toolkit/make_builtin_toolkit.py b/backend/core/tools/make_builtin_toolkit/make_builtin_toolkit.py index 40c918eb..286458a4 100644 --- a/backend/core/tools/make_builtin_toolkit/make_builtin_toolkit.py +++ b/backend/core/tools/make_builtin_toolkit/make_builtin_toolkit.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Optional, Callable from typeguard import typechecked from backend.core.tools.shared_structs.Toolkit import Toolkit from backend.core.tools.make_builtin_toolkit.open_swarm_toolkits.agents_toolkit.make_agents_toolkit import make_agents_toolkit @@ -13,20 +13,27 @@ from backend.core.tools.make_builtin_toolkit.pre_existing_toolkits.meta_toolkits from backend.core.tools.make_builtin_toolkit.pre_existing_toolkits.basic_toolkits.SEARCH_TOOLKIT import SEARCH_TOOLKIT from backend.core.tools.make_builtin_toolkit.pre_existing_toolkits.basic_toolkits.SYSTEM_TOOLKIT import SYSTEM_TOOLKIT from backend.core.Agent.Agent import Agent +from backend.core.shared_structs.dashboard.Dashboard import Dashboard @typechecked def make_builtin_toolkit( parent: Agent, agent_registry: Dict[str, Agent], send_browser_command: BrowserCommandFn, + load_dashboard: Optional[Callable[[str], Dashboard]] = None, + save_dashboard: Optional[Callable[[Dashboard], None]] = None, ) -> Toolkit: return Toolkit( name="builtin", description="Builtin tools", nested_toolkits=[ make_agents_toolkit(parent, agent_registry), - make_browser_delegation_toolkit(parent, send_browser_command), - make_browser_actions_toolkit(parent, send_browser_command), + make_browser_delegation_toolkit( + parent, send_browser_command, + load_dashboard=load_dashboard, + save_dashboard=save_dashboard, + ), + make_browser_actions_toolkit(parent.session_id, send_browser_command), FILESYSTEM_TOOLKIT, INTERACTION_TOOLKIT, PLANNING_TOOLKIT, diff --git a/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/make_create_browser_agent_handler.py b/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/make_create_browser_agent_handler.py index 36dff814..f20971d8 100644 --- a/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/make_create_browser_agent_handler.py +++ b/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/make_create_browser_agent_handler.py @@ -1,5 +1,5 @@ import asyncio -from typing import TypedDict +from typing import TypedDict, Callable, Optional from typeguard import typechecked from backend.core.shared_structs.agent.Message.agent_outputs import ToolResponse @@ -7,18 +7,36 @@ from backend.core.Agent.Agent import Agent from backend.core.tools.make_builtin_toolkit.open_swarm_toolkits.browser_toolkit.make_browser_delegation_toolkit.handlers.utils.run_browser_agent import run_browser_agent from backend.core.tools.make_builtin_toolkit.open_swarm_toolkits.browser_toolkit.make_browser_delegation_toolkit.handlers.utils.create_browser_card import create_browser_card from backend.core.shared_structs.browser.BrowserCommandFn import BrowserCommandFn +from backend.core.shared_structs.dashboard.Dashboard import Dashboard class CreateBrowserAgentInput(TypedDict): task: str @typechecked -def make_create_browser_agent_handler(parent: Agent, send_command: BrowserCommandFn, dashboard_id: str = ""): +def make_create_browser_agent_handler( + parent: Agent, + send_command: BrowserCommandFn, + dashboard_id: str = "", + load_dashboard: Optional[Callable[[str], Dashboard]] = None, + save_dashboard: Optional[Callable[[Dashboard], None]] = None, +): async def handler(args: CreateBrowserAgentInput) -> ToolResponse: task: str = args["task"] - browser_id: str = await create_browser_card(dashboard_id=dashboard_id) + if load_dashboard is None or save_dashboard is None: + return { + "content": [{"type": "text", "text": "Error: dashboard store not configured for browser agent creation"}], + "is_error": True, + } + + browser_id: str = await create_browser_card( + dashboard_id=dashboard_id, + load_dashboard=load_dashboard, + save_dashboard=save_dashboard, + emit=parent.on_event, + ) if not browser_id: return { "content": [{"type": "text", "text": "Error: failed to create browser agent"}], diff --git a/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/utils/run_browser_agent.py b/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/utils/run_browser_agent.py index 1eda032e..f758af45 100644 --- a/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/utils/run_browser_agent.py +++ b/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/handlers/utils/run_browser_agent.py @@ -35,7 +35,7 @@ async def run_browser_agent( browser_config = ClaudeAgentOptions( env=parent.config.env, - model=parent.model, + model=parent.config.model, system_prompt=BROWSER_AGENT_SYSTEM_PROMPT, tools=tool_names, mcp_servers=mcp_servers, diff --git a/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/make_browser_delegation_toolkit.py b/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/make_browser_delegation_toolkit.py index 9d9bacbb..40af6ef4 100644 --- a/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/make_browser_delegation_toolkit.py +++ b/backend/core/tools/make_builtin_toolkit/open_swarm_toolkits/browser_toolkit/make_browser_delegation_toolkit/make_browser_delegation_toolkit.py @@ -1,3 +1,4 @@ +from typing import Optional, Callable from typeguard import typechecked from backend.core.tools.shared_structs.Toolkit import Toolkit from backend.core.tools.shared_structs.MCP_Tool import SDK_MCP_Tool @@ -9,9 +10,15 @@ from backend.core.tools.make_builtin_toolkit.open_swarm_toolkits.browser_toolkit ) from backend.core.Agent.Agent import Agent from backend.core.shared_structs.browser.BrowserCommandFn import BrowserCommandFn +from backend.core.shared_structs.dashboard.Dashboard import Dashboard @typechecked -def make_browser_delegation_toolkit(parent: Agent, send_command: BrowserCommandFn) -> Toolkit: +def make_browser_delegation_toolkit( + parent: Agent, + send_command: BrowserCommandFn, + load_dashboard: Optional[Callable[[str], Dashboard]] = None, + save_dashboard: Optional[Callable[[Dashboard], None]] = None, +) -> Toolkit: return Toolkit( name="browser", description="Tools for browser automation", @@ -26,7 +33,11 @@ def make_browser_delegation_toolkit(parent: Agent, send_command: BrowserCommandF permission="allow", server_name="openswarm-browser", input_schema=CreateBrowserAgentInput, - handler=make_create_browser_agent_handler(parent, send_command), + handler=make_create_browser_agent_handler( + parent, send_command, + load_dashboard=load_dashboard, + save_dashboard=save_dashboard, + ), ), SDK_MCP_Tool( name="InvokeBrowserAgent", diff --git a/electron/main.js b/electron/main.js index dba35ec2..f2f24b74 100644 --- a/electron/main.js +++ b/electron/main.js @@ -233,6 +233,12 @@ function createWindow() { mainWindow.loadFile(frontendPath); } + const LOG_LEVELS = ['verbose', 'info', 'warning', 'error']; + mainWindow.webContents.on('console-message', (_event, level, message) => { + const tag = LOG_LEVELS[level] || 'log'; + process.stdout.write(`[frontend:${tag}] ${message}\n`); + }); + mainWindow.webContents.on('will-attach-webview', (_event, webPreferences, _params) => { webPreferences.plugins = true; webPreferences.enableBlinkFeatures = 'EncryptedMedia'; diff --git a/frontend/src/app/pages/AgentChat/hooks/useAgentChat.ts b/frontend/src/app/pages/AgentChat/hooks/useAgentChat.ts index 2754a320..6d10fdab 100644 --- a/frontend/src/app/pages/AgentChat/hooks/useAgentChat.ts +++ b/frontend/src/app/pages/AgentChat/hooks/useAgentChat.ts @@ -81,7 +81,7 @@ export function useAgentChat({ sessionId: sessionIdProp }: UseAgentChatParams) { }) ).then((action) => { if (META_LAUNCH_AND_SEND.fulfilled.match(action)) { - const realId = action.payload.session.id; + const realId = action.payload.session.session_id; // TODO: Implement title generation // dispatch(generateTitle({ // sessionId: realId, diff --git a/frontend/src/app/pages/AgentChat/thread/MessageActions.tsx b/frontend/src/app/pages/AgentChat/thread/MessageActions.tsx index 879735ad..305d8100 100644 --- a/frontend/src/app/pages/AgentChat/thread/MessageActions.tsx +++ b/frontend/src/app/pages/AgentChat/thread/MessageActions.tsx @@ -79,8 +79,8 @@ const BranchChatButton: FC = () => { DUPLICATE_SESSION(sessionId), ); if (DUPLICATE_SESSION.fulfilled.match(action)) { - if (onBranchChat) onBranchChat(action.payload.session.id); - else dispatch(setActiveSession(action.payload.session.id)); + if (onBranchChat) onBranchChat(action.payload.session.session_id); + else dispatch(setActiveSession(action.payload.session.session_id)); } }, [sessionId, dispatch, aui, onBranchChat]); diff --git a/frontend/src/app/pages/Dashboard/AgentCard.tsx b/frontend/src/app/pages/Dashboard/AgentCard.tsx index 6ec1935f..482b9b38 100644 --- a/frontend/src/app/pages/Dashboard/AgentCard.tsx +++ b/frontend/src/app/pages/Dashboard/AgentCard.tsx @@ -57,20 +57,20 @@ const AgentCard: React.FC = ({ const el = cardBoxRef.current; if (!el || !onMeasuredHeight) return; const ro = new ResizeObserver((entries) => { - for (const entry of entries) onMeasuredHeight(session.id, entry.contentRect.height); + for (const entry of entries) onMeasuredHeight(session.session_id, entry.contentRect.height); }); ro.observe(el); return () => ro.disconnect(); - }, [session.id, onMeasuredHeight]); - const glowEntry = useAppSelector((s) => s.dashboardLayout.glowingAgentCards[session.id]); + }, [session.session_id, onMeasuredHeight]); + const glowEntry = useAppSelector((s) => s.dashboardLayout.glowingAgentCards[session.session_id]); const isGlowingRedux = !!glowEntry; const glowFading = glowEntry?.fading ?? false; const glowFadeTimer = useRef | null>(null); const dismissGlow = useCallback(() => { if (!isGlowingRedux || glowFading) return; - dispatch(fadeGlowingAgentCard(session.id)); - glowFadeTimer.current = setTimeout(() => dispatch(clearGlowingAgentCard(session.id)), GLOW_FADE_MS + 300); - }, [isGlowingRedux, glowFading, dispatch, session.id]); + dispatch(fadeGlowingAgentCard(session.session_id)); + glowFadeTimer.current = setTimeout(() => dispatch(clearGlowingAgentCard(session.session_id)), GLOW_FADE_MS + 300); + }, [isGlowingRedux, glowFading, dispatch, session.session_id]); useEffect(() => () => { if (glowFadeTimer.current) clearTimeout(glowFadeTimer.current); }, []); const accentColor = c.accent.primary, accentHover = c.accent.hover; const statusStyle = getStatusColors(c)[session.status] || { color: c.text.tertiary, bg: c.bg.secondary }; @@ -86,8 +86,8 @@ const AgentCard: React.FC = ({ dragState.current = { startX: e.clientX, startY: e.clientY, origX: cardX, origY: cardY }; didDrag.current = false; setIsDragging(true); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); - onDragStart?.(session.id, 'agent'); - }, [cardX, cardY, onDragStart, session.id]); + onDragStart?.(session.session_id, 'agent'); + }, [cardX, cardY, onDragStart, session.session_id]); const handleDragPointerMove = useCallback((e: React.PointerEvent) => { if (!dragState.current) return; const rawDx = e.clientX - dragState.current.startX, rawDy = e.clientY - dragState.current.startY; @@ -104,16 +104,16 @@ const AgentCard: React.FC = ({ let finalX = dragState.current.origX + dx; const finalY = dragState.current.origY + dy; if (snapColumn && Math.abs(finalX - snapColumn.x) < SNAP_THRESHOLD) { finalX = snapColumn.x; - dispatch(setCardSize({ sessionId: session.id, width: snapColumn.width, height: cardHeight })); + dispatch(setCardSize({ sessionId: session.session_id, width: snapColumn.width, height: cardHeight })); } - dispatch(setCardPosition({ sessionId: session.id, x: finalX, y: finalY })); + dispatch(setCardPosition({ sessionId: session.session_id, x: finalX, y: finalY })); justDraggedRef.current = true; requestAnimationFrame(() => { justDraggedRef.current = false; }); } onDragEnd?.(dx, dy, didDrag.current); dragState.current = null; didDrag.current = false; setLocalDragPos(null); setIsDragging(false); (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); - }, [zoom, dispatch, session.id, onDragEnd, snapColumn, cardHeight]); + }, [zoom, dispatch, session.session_id, onDragEnd, snapColumn, cardHeight]); const resizeRef = useRef<{ dir: ResizeDir; startX: number; startY: number; origX: number; origY: number; origW: number; origH: number } | null>(null); const [isResizing, setIsResizing] = useState(false); const [localResize, setLocalResize] = useState<{ x: number; y: number; w: number; h: number } | null>(null); @@ -141,15 +141,15 @@ const AgentCard: React.FC = ({ const handleResizeUp = useCallback((e: React.PointerEvent) => { if (!resizeRef.current) return; const r = computeResize(e); - if (r) { dispatch(setCardPosition({ sessionId: session.id, x: r.x, y: r.y })); dispatch(setCardSize({ sessionId: session.id, width: r.w, height: r.h })); } + if (r) { dispatch(setCardPosition({ sessionId: session.session_id, x: r.x, y: r.y })); dispatch(setCardSize({ sessionId: session.session_id, width: r.w, height: r.h })); } resizeRef.current = null; setLocalResize(null); setIsResizing(false); (e.target as HTMLElement).releasePointerCapture(e.pointerId); - }, [computeResize, dispatch, session.id]); + }, [computeResize, dispatch, session.session_id]); const handleRemove = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); - dispatch(collapseSession(session.id)); dispatch(removeCard(session.id)); - if (glowEntry) setTimeout(() => dispatch(clearGlowingAgentCard(session.id)), 500); - else dispatch(CLOSE_SESSION(session.id)); + dispatch(collapseSession(session.session_id)); dispatch(removeCard(session.session_id)); + if (glowEntry) setTimeout(() => dispatch(clearGlowingAgentCard(session.session_id)), 500); + else dispatch(CLOSE_SESSION(session.session_id)); }; useEffect(() => { if (session.status === 'running' || session.status === 'waiting_approval') { @@ -187,15 +187,15 @@ const AgentCard: React.FC = ({ onFocusExit?.()} sx={{ color: c.text.ghost }}> - + ); } return ( - onBringToFront?.(session.id, 'agent')} style={{ position: 'absolute', zIndex: isDragging || isResizing ? 999999 : cardZOrder }}> - { if (justDraggedRef.current) return; if (!isSelected && !e.shiftKey) dispatch(toggleExpandSession(session.id)); onCardSelect?.(session.id, 'agent', e.shiftKey); }} + onBringToFront?.(session.session_id, 'agent')} style={{ position: 'absolute', zIndex: isDragging || isResizing ? 999999 : cardZOrder }}> + { if (justDraggedRef.current) return; if (!isSelected && !e.shiftKey) dispatch(toggleExpandSession(session.session_id)); onCardSelect?.(session.session_id, 'agent', e.shiftKey); }} sx={{ position: 'relative', width: localResize ? activeW : Math.max(cardWidth, MIN_W), height: localResize ? activeH : (expanded ? Math.max(EXPANDED_OVERLAY_H, cardHeight) : 'auto'), bgcolor: c.bg.surface, @@ -214,10 +214,10 @@ const AgentCard: React.FC = ({ ))} {isSelected && ( { if (justDraggedRef.current) return; onCardSelect?.(session.id, 'agent', e.shiftKey); }} + onClick={(e: React.MouseEvent) => { if (justDraggedRef.current) return; onCardSelect?.(session.session_id, 'agent', e.shiftKey); }} sx={{ position: 'absolute', inset: 0, zIndex: 15, cursor: isDragging ? 'grabbing' : 'grab', touchAction: 'none' }} /> )} - { e.stopPropagation(); onFocusRequest?.(session.id); }} sx={{ position: 'relative', zIndex: 16, mx: -2, mt: -2, px: 2, pt: 2, pb: 1.5, cursor: isFocused ? 'default' : isDragging ? 'grabbing' : 'grab', touchAction: 'none', userSelect: 'none', flexShrink: 0 }}> + { e.stopPropagation(); onFocusRequest?.(session.session_id); }} sx={{ position: 'relative', zIndex: 16, mx: -2, mt: -2, px: 2, pt: 2, pb: 1.5, cursor: isFocused ? 'default' : isDragging ? 'grabbing' : 'grab', touchAction: 'none', userSelect: 'none', flexShrink: 0 }}> @@ -237,7 +237,7 @@ const AgentCard: React.FC = ({ {expanded && ( e.stopPropagation()} sx={{ mx: -2, mb: -2, flex: 1, minHeight: 0, borderTop: `1px solid ${c.border.subtle}`, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}> - dispatch(collapseSession(session.id))} embedded autoFocus={autoFocusInput} isGlowing={isGlowingRedux && !glowFading} onDismissGlow={dismissGlow} onBranch={onBranch ? (newId: string) => onBranch(session.id, newId) : undefined} /> + dispatch(collapseSession(session.session_id))} embedded autoFocus={autoFocusInput} isGlowing={isGlowingRedux && !glowFading} onDismissGlow={dismissGlow} onBranch={onBranch ? (newId: string) => onBranch(session.session_id, newId) : undefined} /> )} {!expanded && } diff --git a/frontend/src/app/pages/Dashboard/BrowserAgentOverlay.tsx b/frontend/src/app/pages/Dashboard/BrowserAgentOverlay.tsx index 414ef648..03a3092d 100644 --- a/frontend/src/app/pages/Dashboard/BrowserAgentOverlay.tsx +++ b/frontend/src/app/pages/Dashboard/BrowserAgentOverlay.tsx @@ -37,15 +37,15 @@ const BrowserAgentOverlay: React.FC = ({ session, browserWidth, browserHe const isRunning = session.status === 'running'; const isDone = session.status === 'completed' || session.status === 'error' || session.status === 'stopped'; - const prevSessionId = useRef(session.id); + const prevSessionId = useRef(session.session_id); useEffect(() => { - if (session.id !== prevSessionId.current) { - prevSessionId.current = session.id; + if (session.session_id !== prevSessionId.current) { + prevSessionId.current = session.session_id; setFadeOut(false); setHidden(false); setConfirmStop(false); } - }, [session.id]); + }, [session.session_id]); useEffect(() => { if (isDone) { @@ -69,8 +69,8 @@ const BrowserAgentOverlay: React.FC = ({ session, browserWidth, browserHe } if (confirmTimer.current) clearTimeout(confirmTimer.current); setConfirmStop(false); - dispatch(STOP_AGENT(session.id)); - }, [confirmStop, dispatch, session.id]); + dispatch(STOP_AGENT(session.session_id)); + }, [confirmStop, dispatch, session.session_id]); useEffect(() => { return () => { if (confirmTimer.current) clearTimeout(confirmTimer.current); }; diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx b/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx index e3421a1e..c410783c 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx @@ -157,19 +157,19 @@ const DashboardCanvas: React.FC = (p) => { {Object.values(p.cards).map((card: any) => { const session = p.sessions[card.session_id]; - if (!session || p.focusedCardId === session.id) return null; - const extras = getAgentCardExtras(session.id, p.cards, p.glowingAgentCards, + if (!session || p.focusedCardId === session.session_id) return null; + const extras = getAgentCardExtras(session.session_id, p.cards, p.glowingAgentCards, p.expandedSessionIds, p.measuredHeightsRef, p.spawnOriginsRef, p.revealSpawnedRef); return ( - ); @@ -181,7 +181,7 @@ const DashboardCanvas: React.FC = (p) => { return ( = (p) => { { if (META_LAUNCH_AND_SEND.fulfilled.match(action)) { - const realId = action.payload.session.id; + const realId = action.payload.session.session_id; // TODO: Implement title generation // dispatch(generateTitle({ sessionId: realId, prompt })); if (selectedBrowserIds?.length) { diff --git a/frontend/src/shared/backend-bridge/apps/agents.ts b/frontend/src/shared/backend-bridge/apps/agents.ts index a0b0e07f..fbb1675d 100644 --- a/frontend/src/shared/backend-bridge/apps/agents.ts +++ b/frontend/src/shared/backend-bridge/apps/agents.ts @@ -343,15 +343,17 @@ const meta_launch_and_send_endpoint: string = 'agents/meta_launch_and_send'; async function meta_launch_and_send_function( payload: LaunchAndSendPayload, ): Promise<{ draftId: string; session: AgentSession }> { + console.log(`[FRONTEND] meta_launch_and_send: starting | draftId=${payload.draftId} model=${payload.model} mode=${payload.mode} dashboard_id=${payload.config.dashboard_id}`); const { session } = await launch_agent_function({ model: payload.model, mode: payload.mode, system_prompt: payload.config.system_prompt ?? '', max_turns: payload.config.max_turns ?? 100, }); + console.log(`[FRONTEND] meta_launch_and_send: launched | draftId=${payload.draftId} → realId=${session.session_id} status=${session.status} dashboard_id=${session.dashboard_id ?? 'NONE'}`); await send_message_function({ - sessionId: session.id, + sessionId: session.session_id, prompt: payload.prompt, mode: payload.mode, model: payload.model, @@ -361,6 +363,7 @@ async function meta_launch_and_send_function( forcedTools: payload.forcedTools, attachedSkills: payload.attachedSkills, }); + console.log(`[FRONTEND] meta_launch_and_send: message sent | session=${session.session_id}`); return { draftId: payload.draftId, session }; } diff --git a/frontend/src/shared/backend-bridge/apps/dashboards.ts b/frontend/src/shared/backend-bridge/apps/dashboards.ts index c9a927e5..e7c30327 100644 --- a/frontend/src/shared/backend-bridge/apps/dashboards.ts +++ b/frontend/src/shared/backend-bridge/apps/dashboards.ts @@ -94,13 +94,20 @@ async function update_dashboard_function(args: { thumbnail?: string; }): Promise { const { dashboardId, ...updates } = args; + const layoutCards = (updates.layout as Record | undefined)?.cards; + console.log(`[FRONTEND] UPDATE_DASHBOARD: PUT /api/dashboards/${dashboardId} | cardKeys=${layoutCards ? Object.keys(layoutCards as object).join(',') : 'none'} hasLayout=${!!updates.layout} hasName=${!!updates.name} hasThumbnail=${!!updates.thumbnail}`); const res = await fetch(`${DASHBOARDS_API}/${dashboardId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); - if (!res.ok) throw new Error(`Dashboard update failed: ${res.status}`); + if (!res.ok) { + const errorBody = await res.text(); + console.error(`[FRONTEND] UPDATE_DASHBOARD: FAILED ${res.status} | body=${errorBody}`); + throw new Error(`Dashboard update failed: ${res.status}`); + } const data = await res.json(); + console.log(`[FRONTEND] UPDATE_DASHBOARD: OK`); return data as Dashboard; } export const UPDATE_DASHBOARD = createAsyncThunk( diff --git a/frontend/src/shared/state/agentsExtraReducers.ts b/frontend/src/shared/state/agentsExtraReducers.ts index 66dc993a..3f3b409e 100644 --- a/frontend/src/shared/state/agentsExtraReducers.ts +++ b/frontend/src/shared/state/agentsExtraReducers.ts @@ -18,6 +18,14 @@ import { META_LAUNCH_AND_SEND, } from '@/shared/backend-bridge/apps/agents'; +function unwrapMessages(session: T): T { + const msgs = session.messages; + return { + ...session, + messages: Array.isArray(msgs) ? msgs : ((msgs as { messages?: unknown[] })?.messages ?? []), + }; +} + export function buildExtraReducers(builder: ActionReducerMapBuilder) { builder .addCase(GET_ALL_SESSIONS.pending, (state) => { @@ -25,18 +33,21 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder }) .addCase(GET_ALL_SESSIONS.fulfilled, (state, action) => { state.loading = false; - const fetchedIds = new Set(action.payload.map((s) => s.id)); + const fetchedIds = new Set(action.payload.map((s) => s.session_id)); const activeStatuses = new Set(['running', 'waiting_approval']); + console.log(`[FRONTEND] GET_ALL_SESSIONS: fetched ${action.payload.length} sessions | fetchedIds=[${[...fetchedIds].join(',')}] localIds=[${Object.keys(state.sessions).join(',')}]`); for (const [id, existing] of Object.entries(state.sessions)) { if (fetchedIds.has(id)) continue; if (existing.status === 'draft') continue; if (state.trackedNotificationIds.includes(id)) continue; if (activeStatuses.has(existing.status)) continue; + console.warn(`[FRONTEND] GET_ALL_SESSIONS: PRUNING session ${id} (status=${existing.status}) — not in fetched set and not protected`); delete state.sessions[id]; } - for (const s of action.payload) { - const existing = state.sessions[s.id]; - state.sessions[s.id] = { + for (const raw of action.payload) { + const s = unwrapMessages(raw); + const existing = state.sessions[s.session_id]; + state.sessions[s.session_id] = { ...s, pending_approvals: existing?.pending_approvals?.length ? existing.pending_approvals @@ -44,8 +55,8 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder streamingMessage: existing?.streamingMessage ?? s.streamingMessage ?? null, tool_group_meta: { ...existing?.tool_group_meta, ...s.tool_group_meta }, }; - if (activeStatuses.has(s.status) && !state.trackedNotificationIds.includes(s.id)) { - state.trackedNotificationIds.push(s.id); + if (activeStatuses.has(s.status) && !state.trackedNotificationIds.includes(s.session_id)) { + state.trackedNotificationIds.push(s.session_id); } } }) @@ -53,30 +64,33 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder state.loading = false; }) .addCase(LAUNCH_AGENT.fulfilled, (state, action) => { - const session = action.payload.session; - state.sessions[session.id] = { ...session, streamingMessage: null, tool_group_meta: session.tool_group_meta ?? {} }; - state.activeSessionId = session.id; - if (!state.expandedSessionIds.includes(session.id)) { - state.expandedSessionIds.push(session.id); + const session = unwrapMessages(action.payload.session); + state.sessions[session.session_id] = { ...session, streamingMessage: null, tool_group_meta: session.tool_group_meta ?? {} }; + state.activeSessionId = session.session_id; + if (!state.expandedSessionIds.includes(session.session_id)) { + state.expandedSessionIds.push(session.session_id); } - if (!state.trackedNotificationIds.includes(session.id)) { - state.trackedNotificationIds.push(session.id); + if (!state.trackedNotificationIds.includes(session.session_id)) { + state.trackedNotificationIds.push(session.session_id); } }) // TODO: Re-implement this??? .addCase(META_LAUNCH_AND_SEND.fulfilled, (state, action) => { - const { draftId, session } = action.payload; + const { draftId } = action.payload; + const session = unwrapMessages(action.payload.session); const shouldExpand = action.meta.arg.expand !== false; + console.log(`[FRONTEND] agents: META_LAUNCH_AND_SEND fulfilled | draftId=${draftId} realId=${session.session_id} status=${session.status} dashboard_id=${session.dashboard_id ?? 'NONE'} draftExists=${!!state.sessions[draftId]} sessionKeys=[${Object.keys(state.sessions).join(',')}]`); delete state.sessions[draftId]; - state.sessions[session.id] = { ...session, streamingMessage: null, tool_group_meta: session.tool_group_meta ?? {} }; - state.activeSessionId = session.id; - state.expandedSessionIds = state.expandedSessionIds.map((id) => (id === draftId ? session.id : id)); - if (shouldExpand && !state.expandedSessionIds.includes(session.id)) { - state.expandedSessionIds.push(session.id); + state.sessions[session.session_id] = { ...session, streamingMessage: null, tool_group_meta: session.tool_group_meta ?? {} }; + state.activeSessionId = session.session_id; + state.expandedSessionIds = state.expandedSessionIds.map((id) => (id === draftId ? session.session_id : id)); + if (shouldExpand && !state.expandedSessionIds.includes(session.session_id)) { + state.expandedSessionIds.push(session.session_id); } - if (!state.trackedNotificationIds.includes(session.id)) { - state.trackedNotificationIds.push(session.id); + if (!state.trackedNotificationIds.includes(session.session_id)) { + state.trackedNotificationIds.push(session.session_id); } + console.log(`[FRONTEND] agents: session swapped | active=${state.activeSessionId} expanded=[${state.expandedSessionIds.join(',')}] newSessionKeys=[${Object.keys(state.sessions).join(',')}]`); }) // TODO: Re-implement this??? // .addCase(generateTitle.fulfilled, (state, action) => { @@ -130,15 +144,15 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder if (session) session.active_branch_id = action.payload.branchId; }) .addCase(DUPLICATE_SESSION.fulfilled, (state, action) => { - const session = action.payload.session; - state.sessions[session.id] = session; + const session = unwrapMessages(action.payload.session); + state.sessions[session.session_id] = session; }) .addCase(CLOSE_SESSION.fulfilled, (state, action) => { const sessionId = action.payload; const session = state.sessions[sessionId]; if (session) { state.history[sessionId] = { - id: session.id, name: session.name, + id: session.session_id, name: session.name, status: session.status === 'running' || session.status === 'waiting_approval' ? 'stopped' : session.status, model: session.model, mode: session.mode, created_at: session.created_at, closed_at: new Date().toISOString(), cost_usd: session.cost_usd, dashboard_id: session.dashboard_id, @@ -154,7 +168,7 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder const session = state.sessions[sessionId]; if (session) { state.history[sessionId] = { - id: session.id, name: session.name, + id: session.session_id, name: session.name, status: session.status === 'running' || session.status === 'waiting_approval' ? 'stopped' : session.status, model: session.model, mode: session.mode, created_at: session.created_at, closed_at: new Date().toISOString(), cost_usd: session.cost_usd, dashboard_id: session.dashboard_id, @@ -174,16 +188,16 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder state.trackedNotificationIds = state.trackedNotificationIds.filter((id) => id !== sessionId); }) .addCase(RESUME_SESSION.fulfilled, (state, action) => { - const session = action.payload; - state.sessions[session.id] = { ...session, streamingMessage: null, tool_group_meta: session.tool_group_meta ?? {} }; - delete state.history[session.id]; - state.activeSessionId = session.id; - if (!state.expandedSessionIds.includes(session.id)) state.expandedSessionIds.push(session.id); + const session = unwrapMessages(action.payload); + state.sessions[session.session_id] = { ...session, streamingMessage: null, tool_group_meta: session.tool_group_meta ?? {} }; + delete state.history[session.session_id]; + state.activeSessionId = session.session_id; + if (!state.expandedSessionIds.includes(session.session_id)) state.expandedSessionIds.push(session.session_id); }) .addCase(GET_SESSION.fulfilled, (state, action) => { - const session = action.payload; - const existing = state.sessions[session.id]; - state.sessions[session.id] = { + const session = unwrapMessages(action.payload); + const existing = state.sessions[session.session_id]; + state.sessions[session.session_id] = { ...session, pending_approvals: session.pending_approvals ?? existing?.pending_approvals ?? [], streamingMessage: existing?.streamingMessage ?? null, diff --git a/frontend/src/shared/state/agentsReducers.ts b/frontend/src/shared/state/agentsReducers.ts index d595def8..6ac9ade5 100644 --- a/frontend/src/shared/state/agentsReducers.ts +++ b/frontend/src/shared/state/agentsReducers.ts @@ -9,7 +9,7 @@ export const agentsReducers = { reducer(state: AgentsState, action: PayloadAction<{ draftId: string; mode: string; setActive: boolean; targetDirectory?: string }>) { const { draftId, mode, setActive, targetDirectory } = action.payload; state.sessions[draftId] = { - id: draftId, name: 'New chat', status: 'draft', provider: 'anthropic', model: 'sonnet', mode, + session_id: draftId, name: 'New chat', status: 'draft', provider: 'anthropic', model: 'sonnet', mode, worktree_path: null, branch_name: null, sdk_session_id: null, system_prompt: null, allowed_tools: [], max_turns: null, created_at: new Date().toISOString(), cost_usd: 0, tokens: { input: 0, output: 0 }, messages: [], pending_approvals: [], @@ -81,25 +81,31 @@ export const agentsReducers = { }, updateSession(state: AgentsState, action: PayloadAction) { - if (state.history[action.payload.id]) { + const inHistory = !!state.history[action.payload.session_id]; + const existsInSessions = !!state.sessions[action.payload.session_id]; + console.log(`[FRONTEND] updateSession: id=${action.payload.session_id} status=${action.payload.status} inHistory=${inHistory} existsInSessions=${existsInSessions} dashboard_id=${action.payload.dashboard_id ?? 'NONE'}`); + if (state.history[action.payload.session_id]) { if (action.payload.status === 'running' || action.payload.mode === 'browser-agent') { - delete state.history[action.payload.id]; + delete state.history[action.payload.session_id]; } else { + console.log(`[FRONTEND] updateSession: SKIPPED — session in history and not running/browser-agent`); return; } } - const existing = state.sessions[action.payload.id]; + const existing = state.sessions[action.payload.session_id]; const mergedApprovals = existing?.pending_approvals?.length && !action.payload.pending_approvals?.length ? existing.pending_approvals : action.payload.pending_approvals ?? []; - state.sessions[action.payload.id] = { + const msgs = action.payload.messages; + state.sessions[action.payload.session_id] = { ...action.payload, + messages: Array.isArray(msgs) ? msgs : ((msgs as unknown as { messages?: AgentMessage[] })?.messages ?? []), pending_approvals: mergedApprovals, streamingMessage: existing?.streamingMessage ?? action.payload.streamingMessage ?? null, tool_group_meta: { ...existing?.tool_group_meta, ...action.payload.tool_group_meta }, }; - if (action.payload.status === 'running' && !state.trackedNotificationIds.includes(action.payload.id)) { - state.trackedNotificationIds.push(action.payload.id); + if (action.payload.status === 'running' && !state.trackedNotificationIds.includes(action.payload.session_id)) { + state.trackedNotificationIds.push(action.payload.session_id); } }, updateSessionStatus(state: AgentsState, action: PayloadAction<{ sessionId: string; status: AgentSession['status'] }>) { diff --git a/frontend/src/shared/state/agentsTypes.ts b/frontend/src/shared/state/agentsTypes.ts index eb525b82..b4ea5f39 100644 --- a/frontend/src/shared/state/agentsTypes.ts +++ b/frontend/src/shared/state/agentsTypes.ts @@ -42,7 +42,7 @@ export interface ToolGroupMeta { } export interface AgentSession { - id: string; + session_id: string; name: string; status: 'draft' | 'running' | 'waiting_approval' | 'completed' | 'error' | 'stopped'; provider: string; diff --git a/frontend/src/shared/state/dashboardLayoutReducers.ts b/frontend/src/shared/state/dashboardLayoutReducers.ts index 2f0f70ec..56d40d17 100644 --- a/frontend/src/shared/state/dashboardLayoutReducers.ts +++ b/frontend/src/shared/state/dashboardLayoutReducers.ts @@ -23,14 +23,14 @@ export const dashboardLayoutReducers = { }, placeCard(state: S, action: PayloadAction<{ sessionId: string; x: number; y: number; width: number; height: number }>) { const { sessionId, x, y, width, height } = action.payload; - state.cards[sessionId] = { session_id: sessionId, x, y, width, height, zOrder: state.nextZOrder++ }; + state.cards[sessionId] = { session_id: sessionId, x, y, width, height, z_order: state.nextZOrder++ }; }, bringToFront(state: S, action: PayloadAction<{ id: string; type: 'agent' | 'view' | 'browser' }>) { const { id, type } = action.payload; const z = state.nextZOrder++; - if (type === 'agent') { const c = state.cards[id]; if (c) c.zOrder = z; } - else if (type === 'view') { const c = state.viewCards[id]; if (c) c.zOrder = z; } - else { const c = state.browserCards[id]; if (c) c.zOrder = z; } + if (type === 'agent') { const c = state.cards[id]; if (c) c.z_order = z; } + else if (type === 'view') { const c = state.viewCards[id]; if (c) c.z_order = z; } + else { const c = state.browserCards[id]; if (c) c.z_order = z; } }, removeCard(state: S, action: PayloadAction) { delete state.cards[action.payload]; @@ -51,12 +51,12 @@ export const dashboardLayoutReducers = { if (hasDraftCard && !id.startsWith('draft-')) continue; const savedPos = state.closedCardPositions[id]; if (savedPos) { - state.cards[id] = { ...savedPos, session_id: id, zOrder: savedPos.zOrder || state.nextZOrder++ }; + state.cards[id] = { ...savedPos, session_id: id, z_order: savedPos.z_order || state.nextZOrder++ }; delete state.closedCardPositions[id]; } else { const rects = collectOccupiedRects(state, expandedSessionIds); const pos = findOpenGridCell(rects, DEFAULT_CARD_W, DEFAULT_CARD_H); - state.cards[id] = { session_id: id, ...pos, width: DEFAULT_CARD_W, height: DEFAULT_CARD_H, zOrder: state.nextZOrder++ }; + state.cards[id] = { session_id: id, ...pos, width: DEFAULT_CARD_W, height: DEFAULT_CARD_H, z_order: state.nextZOrder++ }; } } }, @@ -90,7 +90,7 @@ export const dashboardLayoutReducers = { const pos = findOpenGridCell(collectOccupiedRects(state, expandedSessionIds), DEFAULT_VIEW_CARD_W, DEFAULT_VIEW_CARD_H); posX = pos.x; posY = pos.y; } - state.viewCards[outputId] = { output_id: outputId, x: posX, y: posY, width: width || DEFAULT_VIEW_CARD_W, height: height || DEFAULT_VIEW_CARD_H, zOrder: state.nextZOrder++ }; + state.viewCards[outputId] = { output_id: outputId, x: posX, y: posY, width: width || DEFAULT_VIEW_CARD_W, height: height || DEFAULT_VIEW_CARD_H, z_order: state.nextZOrder++ }; }, setViewCardPosition(state: S, action: PayloadAction<{ outputId: string; x: number; y: number }>) { const c = state.viewCards[action.payload.outputId]; @@ -109,13 +109,13 @@ export const dashboardLayoutReducers = { state.browserCards[id] = { browser_id: id, url: action.payload.url, tabs: [{ id: tabId, url: action.payload.url, title: '' }], activeTabId: tabId, - ...pos, width: DEFAULT_BROWSER_CARD_W, height: DEFAULT_BROWSER_CARD_H, zOrder: state.nextZOrder++, + ...pos, width: DEFAULT_BROWSER_CARD_W, height: DEFAULT_BROWSER_CARD_H, z_order: state.nextZOrder++, }; }, addBrowserCardFromBackend(state: S, action: PayloadAction) { const c = action.payload; if (state.browserCards[c.browser_id]) return; - state.browserCards[c.browser_id] = { ...c, width: c.width || DEFAULT_BROWSER_CARD_W, height: c.height || DEFAULT_BROWSER_CARD_H, zOrder: c.zOrder || state.nextZOrder++ }; + state.browserCards[c.browser_id] = { ...c, width: c.width || DEFAULT_BROWSER_CARD_W, height: c.height || DEFAULT_BROWSER_CARD_H, z_order: c.z_order || state.nextZOrder++ }; }, setBrowserCardPosition(state: S, action: PayloadAction<{ browserId: string; x: number; y: number }>) { const c = state.browserCards[action.payload.browserId]; @@ -141,7 +141,7 @@ export const dashboardLayoutReducers = { browser_id: id, url: activeTab?.url || action.payload.url, tabs: newTabs.length > 0 ? newTabs : [{ id: generateTabId(), url: action.payload.url, title: '' }], activeTabId: activeTab?.id || generateTabId(), - x: posX, y: posY, width: width || DEFAULT_BROWSER_CARD_W, height: height || DEFAULT_BROWSER_CARD_H, zOrder: state.nextZOrder++, + x: posX, y: posY, width: width || DEFAULT_BROWSER_CARD_W, height: height || DEFAULT_BROWSER_CARD_H, z_order: state.nextZOrder++, }; }, updateBrowserCardUrl(state: S, action: PayloadAction<{ browserId: string; url: string }>) { diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index bf0499dd..ebe3ef9f 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -39,16 +39,16 @@ const dashboardLayoutSlice = createSlice({ let maxZ = 0; for (const c of Object.values(state.cards)) { - if (!c.zOrder) c.zOrder = 0; - if (c.zOrder > maxZ) maxZ = c.zOrder; + if (!c.z_order) c.z_order = 0; + if (c.z_order > maxZ) maxZ = c.z_order; } for (const c of Object.values(state.viewCards)) { - if (!c.zOrder) c.zOrder = 0; - if (c.zOrder > maxZ) maxZ = c.zOrder; + if (!c.z_order) c.z_order = 0; + if (c.z_order > maxZ) maxZ = c.z_order; } for (const c of Object.values(state.browserCards)) { - if (!c.zOrder) c.zOrder = 0; - if (c.zOrder > maxZ) maxZ = c.zOrder; + if (!c.z_order) c.z_order = 0; + if (c.z_order > maxZ) maxZ = c.z_order; } state.nextZOrder = maxZ + 1; }) @@ -59,9 +59,13 @@ const dashboardLayoutSlice = createSlice({ .addCase(META_LAUNCH_AND_SEND.fulfilled, (state, action) => { const { draftId, session } = action.payload; const card = state.cards[draftId]; + console.log(`[FRONTEND] dashboardLayout: META_LAUNCH_AND_SEND fulfilled | draftId=${draftId} realId=${session.session_id} draftCardExists=${!!card} allCardKeys=[${Object.keys(state.cards).join(',')}]`); if (card) { delete state.cards[draftId]; - state.cards[session.id] = { ...card, session_id: session.id, zOrder: state.nextZOrder++ }; + state.cards[session.session_id] = { ...card, session_id: session.session_id, z_order: state.nextZOrder++ }; + console.log(`[FRONTEND] dashboardLayout: card swapped | ${draftId} → ${session.session_id} | newCardKeys=[${Object.keys(state.cards).join(',')}]`); + } else { + console.warn(`[FRONTEND] dashboardLayout: NO CARD FOUND for draftId=${draftId} — card swap SKIPPED`); } }); }, diff --git a/frontend/src/shared/state/dashboardLayoutTypes.ts b/frontend/src/shared/state/dashboardLayoutTypes.ts index 5f7bec51..b574a489 100644 --- a/frontend/src/shared/state/dashboardLayoutTypes.ts +++ b/frontend/src/shared/state/dashboardLayoutTypes.ts @@ -15,7 +15,7 @@ export interface CardPosition { y: number; width: number; height: number; - zOrder: number; + z_order: number; } export interface ViewCardPosition { @@ -24,7 +24,7 @@ export interface ViewCardPosition { y: number; width: number; height: number; - zOrder: number; + z_order: number; } export interface BrowserTab { @@ -43,7 +43,7 @@ export interface BrowserCardPosition { y: number; width: number; height: number; - zOrder: number; + z_order: number; } export interface DashboardLayoutState { diff --git a/frontend/src/shared/ws/wsEventHandlers.ts b/frontend/src/shared/ws/wsEventHandlers.ts index 9a4a8ada..e8a7c4d0 100644 --- a/frontend/src/shared/ws/wsEventHandlers.ts +++ b/frontend/src/shared/ws/wsEventHandlers.ts @@ -31,11 +31,19 @@ interface WsDeltaCallbacks { export function dispatchWsEvent(msg: WSEvent, delta: WsDeltaCallbacks): void { const { event, session_id, data } = msg; + if (event !== 'agent:stream_delta') { + console.log(`[FRONTEND] WS event: ${event} | session=${session_id ?? 'none'} | dataKeys=${Object.keys(data).join(',')}`); + } + switch (event) { case 'agent:status': if (data.session) { - store.dispatch(updateSession(data.session)); + const s = data.session; + console.log(`[FRONTEND] WS agent:status full session | id=${s.session_id} status=${s.status} dashboard_id=${s.dashboard_id ?? 'NONE'}`); + if (s.messages && !Array.isArray(s.messages)) s.messages = s.messages.messages ?? []; + store.dispatch(updateSession(s)); } else if (session_id) { + console.log(`[FRONTEND] WS agent:status update | id=${session_id} status=${data.status}`); store.dispatch(updateSessionStatus({ sessionId: session_id, status: data.status })); } if (data.status === 'running' && session_id) { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index cae2f7f2..430ff216 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ '/api': { target: `http://localhost:${portsConfig.backend.dev}`, changeOrigin: true, + ws: true, }, }, },