[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:
haikdc
2026-04-18 10:26:51 -07:00
parent 563af6ff80
commit f98c56ab7e
30 changed files with 281 additions and 141 deletions
@@ -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:
+12 -5
View File
@@ -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,
+4 -1
View File
@@ -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)
+1 -1
View File
@@ -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:
+14 -3
View File
@@ -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,
@@ -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"}],
@@ -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,
@@ -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",
+6
View File
@@ -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]);
+23 -23
View File
@@ -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,
+13 -7
View File
@@ -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'] }>) {
+1 -1
View File
@@ -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 {
+9 -1
View File
@@ -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) {
+1
View File
@@ -20,6 +20,7 @@ export default defineConfig({
'/api': {
target: `http://localhost:${portsConfig.backend.dev}`,
changeOrigin: true,
ws: true,
},
},
},