mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[hAIk]: Holy fucking shit the agent finally works in the frontend: align frontend/backend naming: rename AgentSession.id→session_id across agentsTypes, agentsReducers, agentsExtraReducers, AgentCard, BrowserAgentOverlay, DashboardCanvas, useAgentChat, useToolbarActions, useDashboardKeyboard, MessageActions, and backend-bridge agents/dashboards; rename CardPosition.zOrder→z_order in dashboardLayoutTypes, dashboardLayoutReducers, dashboardLayoutSlice, and DashboardCanvas; fix circular import between dashboards.py and agents.py by deferring imports; add NINE_ROUTER_MODEL_MAP for sonnet/opus/haiku alias resolution in launch_agent; make build_agent_toolkit async and pass load_dashboard/save_dashboard into browser delegation toolkit; fix PromptMsgDict to use TypedDict, fix zip type hint bug, and fix browser agent model reference (parent.model→parent.config.model); add p_on_task_done callback with traceback logging on Agent, capture stderr and full tracebacks in run_agent_loop; add unwrages helper in agentsExtraReducers to normalize nested message payloads; pipe frontend console-message events to Electron stdout; enable ws:true on Vite proxy; add diagnostic console.logs across WS handlers, session lifecycle, and dashboard updates
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
height: float = 280
|
||||
z_order: int = 0
|
||||
@@ -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,
|
||||
|
||||
+21
-3
@@ -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"}],
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+13
-2
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -57,20 +57,20 @@ const AgentCard: React.FC<Props> = ({
|
||||
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<ReturnType<typeof setTimeout> | 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<Props> = ({
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
</Box>
|
||||
<IconButton size="small" onClick={() => onFocusExit?.()} sx={{ color: c.text.ghost }}><CloseIcon sx={{ fontSize: 18 }} /></IconButton>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minHeight: 0, overflow: 'hidden' }}><AgentChat sessionId={session.id} autoFocus={true} embedded={true} /></Box>
|
||||
<Box sx={{ flex: 1, minHeight: 0, overflow: 'hidden' }}><AgentChat sessionId={session.session_id} autoFocus={true} embedded={true} /></Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div layout={false} initial={spawnInitial} animate={{ opacity: 1, scale: 1, left: activeX, top: activeY }} exit={exitAnimation} transition={spawnTransition} onPointerDownCapture={() => onBringToFront?.(session.id, 'agent')} style={{ position: 'absolute', zIndex: isDragging || isResizing ? 999999 : cardZOrder }}>
|
||||
<Box ref={cardBoxRef} data-select-type="agent-card" data-select-id={session.id} data-select-meta={JSON.stringify({ name: session.name || session.id, status: session.status, model: session.model, mode: session.mode })}
|
||||
onClick={(e: React.MouseEvent) => { if (justDraggedRef.current) return; if (!isSelected && !e.shiftKey) dispatch(toggleExpandSession(session.id)); onCardSelect?.(session.id, 'agent', e.shiftKey); }}
|
||||
<motion.div layout={false} initial={spawnInitial} animate={{ opacity: 1, scale: 1, left: activeX, top: activeY }} exit={exitAnimation} transition={spawnTransition} onPointerDownCapture={() => onBringToFront?.(session.session_id, 'agent')} style={{ position: 'absolute', zIndex: isDragging || isResizing ? 999999 : cardZOrder }}>
|
||||
<Box ref={cardBoxRef} data-select-type="agent-card" data-select-id={session.session_id} data-select-meta={JSON.stringify({ name: session.name || session.session_id, status: session.status, model: session.model, mode: session.mode })}
|
||||
onClick={(e: React.MouseEvent) => { 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<Props> = ({
|
||||
))}
|
||||
{isSelected && (
|
||||
<Box ref={scrollOverlayRef} onPointerDown={handleDragPointerDown} onPointerMove={handleDragPointerMove} onPointerUp={handleDragPointerUp}
|
||||
onClick={(e: React.MouseEvent) => { 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' }} />
|
||||
)}
|
||||
<Box onPointerDown={handleDragPointerDown} onPointerMove={handleDragPointerMove} onPointerUp={handleDragPointerUp} onDoubleClick={(e) => { 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 }}>
|
||||
<Box onPointerDown={handleDragPointerDown} onPointerMove={handleDragPointerMove} onPointerUp={handleDragPointerUp} onDoubleClick={(e) => { 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 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1, flexShrink: 0 }}>
|
||||
<Box className="drag-handle" sx={{ display: 'flex', alignItems: 'center', mr: 0.5, color: c.text.ghost }}><DragIndicatorIcon sx={{ fontSize: 16 }} /></Box>
|
||||
<Box sx={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 1, borderRadius: 1 }}>
|
||||
@@ -237,7 +237,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
</Box>
|
||||
{expanded && (
|
||||
<Box onClick={(e) => e.stopPropagation()} sx={{ mx: -2, mb: -2, flex: 1, minHeight: 0, borderTop: `1px solid ${c.border.subtle}`, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<AgentChat key={session.id} sessionId={session.id} onClose={() => dispatch(collapseSession(session.id))} embedded autoFocus={autoFocusInput} isGlowing={isGlowingRedux && !glowFading} onDismissGlow={dismissGlow} onBranch={onBranch ? (newId: string) => onBranch(session.id, newId) : undefined} />
|
||||
<AgentChat key={session.session_id} sessionId={session.session_id} onClose={() => dispatch(collapseSession(session.session_id))} embedded autoFocus={autoFocusInput} isGlowing={isGlowingRedux && !glowFading} onDismissGlow={dismissGlow} onBranch={onBranch ? (newId: string) => onBranch(session.session_id, newId) : undefined} />
|
||||
</Box>
|
||||
)}
|
||||
{!expanded && <AgentCardCollapsed session={session} previewContent={previewContent} isStreaming={isStreaming} hasPending={hasPending} statusStyle={statusStyle} c={c} />}
|
||||
|
||||
@@ -37,15 +37,15 @@ const BrowserAgentOverlay: React.FC<Props> = ({ 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<Props> = ({ 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); };
|
||||
|
||||
@@ -157,19 +157,19 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = (p) => {
|
||||
<AnimatePresence>
|
||||
{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 (
|
||||
<AgentCard key={session.id} session={session} expanded={p.expandedSessionIds.includes(session.id)}
|
||||
<AgentCard key={session.session_id} session={session} expanded={p.expandedSessionIds.includes(session.session_id)}
|
||||
cardX={card.x} cardY={card.y} cardWidth={card.width} cardHeight={card.height}
|
||||
cardZOrder={card.zOrder ?? 0} zoom={p.zoom} spawnFrom={extras.origin}
|
||||
exitTarget={extras.exitTarget} isSelected={p.isSelected(session.id)}
|
||||
isHighlighted={p.highlightedCardId === session.id} multiDragDelta={p.multiDragDelta}
|
||||
cardZOrder={card.z_order ?? 0} zoom={p.zoom} spawnFrom={extras.origin}
|
||||
exitTarget={extras.exitTarget} isSelected={p.isSelected(session.session_id)}
|
||||
isHighlighted={p.highlightedCardId === session.session_id} multiDragDelta={p.multiDragDelta}
|
||||
onCardSelect={p.handleCardSelect} onDragStart={p.handleCardDragStart}
|
||||
onDragMove={p.handleCardDragMove} onDragEnd={p.handleCardDragEnd}
|
||||
onBranch={p.handleBranchFromCard} onMeasuredHeight={p.handleMeasuredHeight}
|
||||
snapColumn={extras.snapColumn} autoFocusInput={p.autoFocusSessionId === session.id}
|
||||
snapColumn={extras.snapColumn} autoFocusInput={p.autoFocusSessionId === session.session_id}
|
||||
onBringToFront={p.handleBringToFront} isFocused={false}
|
||||
onFocusRequest={p.handleFocusRequest} onFocusExit={p.handleFocusExit} />
|
||||
);
|
||||
@@ -181,7 +181,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = (p) => {
|
||||
return (
|
||||
<DashboardViewCard key={`view-${vc.output_id}`} output={output}
|
||||
cardX={vc.x} cardY={vc.y} cardWidth={vc.width} cardHeight={vc.height}
|
||||
cardZOrder={vc.zOrder ?? 0} zoom={p.zoom} cmdHeld={p.cmdHeld}
|
||||
cardZOrder={vc.z_order ?? 0} zoom={p.zoom} cmdHeld={p.cmdHeld}
|
||||
isSelected={p.isSelected(vc.output_id)} isHighlighted={p.highlightedCardId === vc.output_id}
|
||||
multiDragDelta={p.multiDragDelta} onCardSelect={p.handleCardSelect}
|
||||
onDragStart={p.handleCardDragStart} onDragMove={p.handleCardDragMove}
|
||||
@@ -192,7 +192,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = (p) => {
|
||||
<BrowserCard key={`browser-${bc.browser_id}`} browserId={bc.browser_id}
|
||||
tabs={bc.tabs} activeTabId={bc.activeTabId}
|
||||
cardX={bc.x} cardY={bc.y} cardWidth={bc.width} cardHeight={bc.height}
|
||||
cardZOrder={bc.zOrder ?? 0} zoom={p.zoom} cmdHeld={p.cmdHeld}
|
||||
cardZOrder={bc.z_order ?? 0} zoom={p.zoom} cmdHeld={p.cmdHeld}
|
||||
isSelected={p.isSelected(bc.browser_id)} isHighlighted={p.highlightedCardId === bc.browser_id}
|
||||
multiDragDelta={p.multiDragDelta} onCardSelect={p.handleCardSelect}
|
||||
onDragStart={p.handleCardDragStart} onDragMove={p.handleCardDragMove}
|
||||
|
||||
@@ -181,7 +181,7 @@ export function useDashboardKeyboard(deps: KeyboardDeps) {
|
||||
if (card.type === 'agent') {
|
||||
const action = await dispatch(DUPLICATE_SESSION(card.id));
|
||||
if (DUPLICATE_SESSION.fulfilled.match(action)) {
|
||||
const newId = action.payload.session.id;
|
||||
const newId = action.payload.session.session_id;
|
||||
dispatch(placeCard({ sessionId: newId, x: px, y: py, width: card.width, height: card.height }));
|
||||
if (card.expanded) dispatch(expandSession(newId));
|
||||
newSelection.set(newId, 'agent');
|
||||
|
||||
@@ -102,6 +102,8 @@ export function useToolbarActions(deps: ToolbarDeps) {
|
||||
};
|
||||
}
|
||||
const config: AgentConfig = { name: 'New chat', model, mode, dashboard_id: dashboardId };
|
||||
const origin = spawnOriginsRef.current[draftId] ?? { x: 0, y: 0 };
|
||||
dispatch(placeCard({ sessionId: draftId, x: origin.x, y: origin.y, width: DEFAULT_CARD_W, height: DEFAULT_CARD_H }));
|
||||
dispatch(
|
||||
META_LAUNCH_AND_SEND({
|
||||
draftId, config, prompt, mode, model, images,
|
||||
@@ -110,7 +112,7 @@ export function useToolbarActions(deps: ToolbarDeps) {
|
||||
}),
|
||||
).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, prompt }));
|
||||
if (selectedBrowserIds?.length) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -94,13 +94,20 @@ async function update_dashboard_function(args: {
|
||||
thumbnail?: string;
|
||||
}): Promise<Dashboard> {
|
||||
const { dashboardId, ...updates } = args;
|
||||
const layoutCards = (updates.layout as Record<string, unknown> | 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(
|
||||
|
||||
@@ -18,6 +18,14 @@ import {
|
||||
META_LAUNCH_AND_SEND,
|
||||
} from '@/shared/backend-bridge/apps/agents';
|
||||
|
||||
function unwrapMessages<T extends { messages: unknown }>(session: T): T {
|
||||
const msgs = session.messages;
|
||||
return {
|
||||
...session,
|
||||
messages: Array.isArray(msgs) ? msgs : ((msgs as { messages?: unknown[] })?.messages ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildExtraReducers(builder: ActionReducerMapBuilder<AgentsState>) {
|
||||
builder
|
||||
.addCase(GET_ALL_SESSIONS.pending, (state) => {
|
||||
@@ -25,18 +33,21 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder<AgentsState>
|
||||
})
|
||||
.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<AgentsState>
|
||||
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<AgentsState>
|
||||
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<AgentsState>
|
||||
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<AgentsState>
|
||||
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<AgentsState>
|
||||
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,
|
||||
|
||||
@@ -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<AgentSession>) {
|
||||
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'] }>) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string>) {
|
||||
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<BrowserCardPosition>) {
|
||||
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 }>) {
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: `http://localhost:${portsConfig.backend.dev}`,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user