diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index b846b5b8..92889ace 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -48,6 +48,14 @@ AGENT_STORE: PydanticStore[Agent] = PydanticStore[Agent]( # NOTE: Essentially the SESSIONS is a cache for active agents. SESSIONS: dict[str, Agent] = {} +def _persist_agent(agent: Agent) -> None: + """Called when an agent reaches a terminal state (completed/error).""" + debug("auto-saving session %s (status=%s)", agent.session_id, agent.status) + try: + AGENT_STORE.save(agent) + except Exception as e: + debug("auto-save failed for session %s: %s", agent.session_id, e) + def get_agent(session_id: str) -> Agent: agent: Optional[Agent] = SESSIONS.get(session_id) if not agent: @@ -64,6 +72,7 @@ async def agents_lifespan(): try: stored.status = "stopped" stored.on_event = COMMS_MANAGER.make_session_emitter(stored.session_id) + stored.on_done = _persist_agent stored.toolkit = await build_agent_toolkit( agent=stored, sessions=SESSIONS, @@ -73,10 +82,15 @@ async def agents_lifespan(): except Exception as e: debug(f"[agents lifespan] Skipping corrupt session {stored.session_id}: {e}") yield + debug("agents_lifespan: shutting down — %s sessions to save", len(SESSIONS)) for agent in list[Agent](SESSIONS.values()): + debug("agents_lifespan: stopping agent %s", agent.session_id) await agent.stop_agent() + debug("agents_lifespan: saving agent %s", agent.session_id) AGENT_STORE.save(agent) + debug("agents_lifespan: saved agent %s", agent.session_id) SESSIONS.clear() + debug("agents_lifespan: shutdown complete") agents = SubApp("agents", agents_lifespan) @@ -115,10 +129,10 @@ async def websocket_dashboard(websocket: WebSocket): # --------------------------------------------------------------------------- @agents.router.get("/get_all_sessions") -async def get_all_sessions(dashboard_id: str = Body(default="")) -> dict: - result: List[Agent] = list[Agent](SESSIONS.values()) +async def get_all_sessions(dashboard_id: str = "") -> dict: + result: List[Agent] = list(SESSIONS.values()) if dashboard_id: - result: List[Agent] = [a for a in result if getattr(a, "dashboard_id", None) == dashboard_id] + result = [a for a in result if a.dashboard_id == dashboard_id] return {"sessions": [a.model_dump(mode="json") for a in result]} @@ -134,14 +148,17 @@ async def launch_agent( mode: str = Body(), system_prompt: str = Body(), max_turns: int = Body(), + dashboard_id: Optional[str] = Body(default=None), ) -> dict: agent: Agent = Agent( model=model, mode=mode, status="stopped", + dashboard_id=dashboard_id, config=ClaudeAgentOptions(max_turns=max_turns), ) agent.on_event = COMMS_MANAGER.make_session_emitter(agent.session_id) + agent.on_done = _persist_agent SESSIONS[agent.session_id] = agent toolkit: Toolkit = await build_agent_toolkit( @@ -372,6 +389,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.on_done = _persist_agent agent.toolkit = await build_agent_toolkit( agent=agent, sessions=SESSIONS, @@ -402,6 +420,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.on_done = _persist_agent clone.toolkit = await build_agent_toolkit( agent=clone, sessions=SESSIONS, diff --git a/backend/core/Agent/Agent.py b/backend/core/Agent/Agent.py index 50a5d4bd..850fe2b6 100644 --- a/backend/core/Agent/Agent.py +++ b/backend/core/Agent/Agent.py @@ -32,7 +32,8 @@ class Agent(BaseModel): messages: MessageLog = Field(default_factory=MessageLog) session_id: str = Field(default_factory=lambda: uuid4().hex) - config: ClaudeAgentOptions + dashboard_id: Optional[str] = None + config: ClaudeAgentOptions = Field(default_factory=ClaudeAgentOptions, exclude=True) branch_id: str = "main" sub_agents: List["Agent"] = Field(default_factory=list) @@ -41,9 +42,10 @@ class Agent(BaseModel): toolkit: Optional[Toolkit] = Field(default=None, exclude=True) on_event: Optional[EventCallback] = Field(default=None, exclude=True) + on_done: Optional[Any] = Field(default=None, exclude=True) - task: Optional[InstanceOf[asyncio.Task]] = None - lock: InstanceOf[asyncio.Lock] = Field(default_factory=asyncio.Lock) + task: Optional[InstanceOf[asyncio.Task]] = Field(default=None, exclude=True) + lock: InstanceOf[asyncio.Lock] = Field(default_factory=asyncio.Lock, exclude=True) @typechecked def snapshot(self) -> AgentSnapshot: @@ -52,6 +54,7 @@ class Agent(BaseModel): model=self.model, mode=self.mode, status=self.status, + dashboard_id=self.dashboard_id, branch_id=self.branch_id, parent_id=self.parent_id, messages=self.messages, @@ -70,6 +73,9 @@ class Agent(BaseModel): self.status = event.status # type: ignore[assignment] if self.on_event: await self.on_event(event) + if isinstance(event, AgentStatusEvent) and event.status in ("completed", "error"): + if self.on_done: + self.on_done(self) @typechecked async def request_approval( diff --git a/backend/core/db/PydanticStore.py b/backend/core/db/PydanticStore.py index cdb572ea..6fb6145c 100644 --- a/backend/core/db/PydanticStore.py +++ b/backend/core/db/PydanticStore.py @@ -6,10 +6,12 @@ inside a data directory. This module eliminates that copy-paste. import json import os +import tempfile from typing import Generic, List, Optional, TypeVar from fastapi import HTTPException from pydantic import BaseModel +from swarm_debug import debug from typeguard import typechecked T = TypeVar("T", bound=BaseModel) @@ -39,19 +41,33 @@ class PydanticStore(BaseModel, Generic[T]): def load_all(self) -> list[T]: result: List[T] = [] if not os.path.exists(self.data_dir): + debug("load_all: data_dir does not exist: %s", self.data_dir) return result - for fname in os.listdir(self.data_dir): - if fname.endswith(".json"): - with open(os.path.join(self.data_dir, fname)) as f: - result.append(self.model_cls(**json.load(f))) + fnames = [f for f in os.listdir(self.data_dir) if f.endswith(".json")] + debug("load_all: scanning %s — found %s json files", self.data_dir, len(fnames), table=False) + for fname in fnames: + path = os.path.join(self.data_dir, fname) + size = os.path.getsize(path) + debug("load_all: loading %s (%s bytes)", fname, size, table=False) + with open(path) as f: + raw = f.read() + debug("load_all: raw content length=%s, first 100 chars: %s", len(raw), raw[:100], table=False) + result.append(self.model_cls(**json.loads(raw))) return result @typechecked def save(self, item: T) -> None: os.makedirs(self.data_dir, exist_ok=True) item_id = getattr(item, self.id_field) - with open(self.p_path(item_id), "w") as f: - json.dump(self.p_dump(item), f, indent=2) + path = self.p_path(item_id) + debug("save: writing %s to %s", item_id, path, table=False) + with tempfile.NamedTemporaryFile( + "w", dir=self.data_dir, suffix=".tmp", delete=False + ) as tmp: + json.dump(self.p_dump(item), tmp, indent=2) + tmp_path = tmp.name + os.replace(tmp_path, path) + debug("save: complete — %s is now %s bytes", path, os.path.getsize(path), table=False) @typechecked def load(self, item_id: str) -> T: diff --git a/backend/core/shared_structs/agent/AgentSnapshot.py b/backend/core/shared_structs/agent/AgentSnapshot.py index da9d6334..0557464b 100644 --- a/backend/core/shared_structs/agent/AgentSnapshot.py +++ b/backend/core/shared_structs/agent/AgentSnapshot.py @@ -9,6 +9,7 @@ class AgentSnapshot(BaseModel): model: str mode: str status: str + dashboard_id: Optional[str] = None branch_id: str = "main" parent_id: Optional[str] = None messages: MessageLog = Field(default_factory=MessageLog) diff --git a/frontend/src/app/pages/Dashboard/AgentCard.tsx b/frontend/src/app/pages/Dashboard/AgentCard/AgentCard.tsx similarity index 98% rename from frontend/src/app/pages/Dashboard/AgentCard.tsx rename to frontend/src/app/pages/Dashboard/AgentCard/AgentCard.tsx index 482b9b38..372c003d 100644 --- a/frontend/src/app/pages/Dashboard/AgentCard.tsx +++ b/frontend/src/app/pages/Dashboard/AgentCard/AgentCard.tsx @@ -13,11 +13,11 @@ import { setCardPosition, setCardSize, fadeGlowingAgentCard, clearGlowingAgentCa import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import AgentChat from '@/app/pages/AgentChat/AgentChat'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { useOverlayScrollPassthrough } from './useOverlayScrollPassthrough'; -import { type ResizeDir, DRAG_THRESHOLD, CURSOR_MAP, HANDLE_DEFS } from './cardLayoutConstants'; -import CardGlowOverlay from './CardGlowOverlay'; -import AgentCardCollapsed from './AgentCardCollapsed'; -import { formatDuration, getStatusColors, getPreviewContent } from './agentCardUtils'; +import { useOverlayScrollPassthrough } from '@/app/pages/Dashboard/useOverlayScrollPassthrough'; +import { type ResizeDir, DRAG_THRESHOLD, CURSOR_MAP, HANDLE_DEFS } from '@/app/pages/Dashboard/cardLayoutConstants'; +import CardGlowOverlay from './components/CardGlowOverlay'; +import AgentCardCollapsed from './components/AgentCardCollapsed'; +import { formatDuration, getStatusColors, getPreviewContent } from './components/agentCardUtils'; interface Props { session: AgentSession; expanded: boolean; diff --git a/frontend/src/app/pages/Dashboard/AgentCardCollapsed.tsx b/frontend/src/app/pages/Dashboard/AgentCard/components/AgentCardCollapsed.tsx similarity index 100% rename from frontend/src/app/pages/Dashboard/AgentCardCollapsed.tsx rename to frontend/src/app/pages/Dashboard/AgentCard/components/AgentCardCollapsed.tsx diff --git a/frontend/src/app/pages/Dashboard/CardGlowOverlay.tsx b/frontend/src/app/pages/Dashboard/AgentCard/components/CardGlowOverlay.tsx similarity index 100% rename from frontend/src/app/pages/Dashboard/CardGlowOverlay.tsx rename to frontend/src/app/pages/Dashboard/AgentCard/components/CardGlowOverlay.tsx diff --git a/frontend/src/app/pages/Dashboard/agentCardUtils.ts b/frontend/src/app/pages/Dashboard/AgentCard/components/agentCardUtils.ts similarity index 100% rename from frontend/src/app/pages/Dashboard/agentCardUtils.ts rename to frontend/src/app/pages/Dashboard/AgentCard/components/agentCardUtils.ts diff --git a/frontend/src/app/pages/Dashboard/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/BrowserCard/BrowserCard.tsx similarity index 96% rename from frontend/src/app/pages/Dashboard/BrowserCard.tsx rename to frontend/src/app/pages/Dashboard/BrowserCard/BrowserCard.tsx index 68a0093f..04fb30cd 100644 --- a/frontend/src/app/pages/Dashboard/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/BrowserCard/BrowserCard.tsx @@ -6,14 +6,15 @@ import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useBrowserActivity } from '@/shared/useBrowserActivity'; import { resolveInput, isGoogleSearch } from '@/shared/resolveUrl'; -import BrowserAgentOverlay from './BrowserAgentOverlay'; -import { useOverlayScrollPassthrough } from './useOverlayScrollPassthrough'; +import BrowserAgentOverlay from './components/BrowserAgentOverlay/BrowserAgentOverlay'; +import { useOverlayScrollPassthrough } from '@/app/pages/Dashboard/useOverlayScrollPassthrough'; import { useElementSelection } from '@/app/components/ElementSelectionContext'; -import { type ResizeDir, CURSOR_MAP, HANDLE_DEFS, DRAG_THRESHOLD } from './cardLayoutConstants'; -import { useWebviewLifecycle, isElectron, chromeUserAgent, webviewPreloadPath, type TabLocalState, type WebviewElement } from './hooks/useWebviewLifecycle'; -import BrowserTabBar from './BrowserTabBar'; -import BrowserNavBar from './BrowserNavBar'; -import BrowserActionOverlay from './BrowserActionOverlay'; +import { type ResizeDir, CURSOR_MAP, HANDLE_DEFS, DRAG_THRESHOLD } from '@/app/pages/Dashboard/cardLayoutConstants'; +import { useWebviewLifecycle, isElectron, chromeUserAgent, webviewPreloadPath, type WebviewElement } from './hooks/useWebviewLifecycle'; +import type { TabLocalState } from '@/app/pages/Dashboard/types/types'; +import BrowserTabBar from './components/BrowserTabBar'; +import BrowserNavBar from './components/BrowserNavBar'; +import BrowserActionOverlay from './components/BrowserActionOverlay'; const MIN_W = 400, MIN_H = 300; diff --git a/frontend/src/app/pages/Dashboard/BrowserActionOverlay.tsx b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserActionOverlay.tsx similarity index 100% rename from frontend/src/app/pages/Dashboard/BrowserActionOverlay.tsx rename to frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserActionOverlay.tsx diff --git a/frontend/src/app/pages/Dashboard/BrowserAgentOverlay.tsx b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/BrowserAgentOverlay.tsx similarity index 83% rename from frontend/src/app/pages/Dashboard/BrowserAgentOverlay.tsx rename to frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/BrowserAgentOverlay.tsx index 03a3092d..849a8053 100644 --- a/frontend/src/app/pages/Dashboard/BrowserAgentOverlay.tsx +++ b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/BrowserAgentOverlay.tsx @@ -9,14 +9,53 @@ import OpenInFullIcon from '@mui/icons-material/OpenInFull'; import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import { AgentSession } from '@/shared/state/agentsSlice'; +import { AgentMessage, AgentSession } from '@/shared/state/agentsSlice'; import { STOP_AGENT } from '@/shared/backend-bridge/apps/agents'; import { useAppDispatch } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { summarizeMessage } from './browserAgentOverlayUtils'; +import { OverlayEntry } from './OverlayEntry'; import OverlayActionLog from './OverlayActionLog'; +export function summarizeMessage(msg: AgentMessage): OverlayEntry { + if (msg.role === 'assistant' && typeof msg.content === 'string') { + const trimmed = msg.content.trim(); + if (!trimmed) return { type: 'skip', text: '' }; + return { type: 'thought', text: trimmed }; + } + + if (msg.role === 'tool_call') { + const content = typeof msg.content === 'string' + ? (() => { try { return JSON.parse(msg.content); } catch { return {}; } })() + : msg.content; + const tool = content?.tool || content?.name || '?'; + const input = content?.input || {}; + let brief = ''; + switch (tool) { + case 'BrowserNavigate': brief = `Navigate → ${input.url || '...'}`; break; + case 'BrowserClick': brief = `Click ${input.selector || '...'}`; break; + case 'BrowserType': + brief = `Type "${(input.text || '').slice(0, 30)}${(input.text || '').length > 30 ? '…' : ''}" into ${input.selector || '...'}`; + break; + case 'BrowserScreenshot': brief = 'Screenshot'; break; + case 'BrowserGetText': brief = 'Read page text'; break; + case 'BrowserGetElements': + brief = `Inspect elements${input.selector ? ` (${input.selector})` : ''}`; + break; + case 'BrowserEvaluate': brief = 'Evaluate JS'; break; + default: brief = tool; + } + return { type: 'action', text: brief }; + } + + if (msg.role === 'tool_result') { + return { type: 'result', text: '' }; + } + + return { type: 'skip', text: '' }; +} + + interface Props { session: AgentSession; browserWidth: number; diff --git a/frontend/src/app/pages/Dashboard/OverlayActionLog.tsx b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/OverlayActionLog.tsx similarity index 98% rename from frontend/src/app/pages/Dashboard/OverlayActionLog.tsx rename to frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/OverlayActionLog.tsx index 3281102e..6d20ef3e 100644 --- a/frontend/src/app/pages/Dashboard/OverlayActionLog.tsx +++ b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/OverlayActionLog.tsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { OverlayEntry } from './browserAgentOverlayUtils'; +import { OverlayEntry } from './OverlayEntry'; interface Props { entries: OverlayEntry[]; diff --git a/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/OverlayEntry.ts b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/OverlayEntry.ts new file mode 100644 index 00000000..f3b09484 --- /dev/null +++ b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserAgentOverlay/OverlayEntry.ts @@ -0,0 +1,4 @@ +export interface OverlayEntry { + type: 'thought' | 'action' | 'result' | 'skip'; + text: string; +} \ No newline at end of file diff --git a/frontend/src/app/pages/Dashboard/BrowserNavBar.tsx b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserNavBar.tsx similarity index 100% rename from frontend/src/app/pages/Dashboard/BrowserNavBar.tsx rename to frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserNavBar.tsx diff --git a/frontend/src/app/pages/Dashboard/BrowserTabBar.tsx b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserTabBar.tsx similarity index 99% rename from frontend/src/app/pages/Dashboard/BrowserTabBar.tsx rename to frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserTabBar.tsx index 4a365f88..81d89e74 100644 --- a/frontend/src/app/pages/Dashboard/BrowserTabBar.tsx +++ b/frontend/src/app/pages/Dashboard/BrowserCard/components/BrowserTabBar.tsx @@ -13,7 +13,7 @@ import { reorderBrowserTab, setActiveBrowserTab, addBrowserTab, removeBrowserTab, removeBrowserCard, type BrowserTab, } from '@/shared/state/dashboardLayoutSlice'; -import type { TabLocalState } from './hooks/useWebviewLifecycle'; +import type { TabLocalState } from '@/app/pages/Dashboard/types/types'; interface BrowserTabBarProps { tabs: BrowserTab[]; diff --git a/frontend/src/app/pages/Dashboard/hooks/useWebviewLifecycle.ts b/frontend/src/app/pages/Dashboard/BrowserCard/hooks/useWebviewLifecycle.ts similarity index 94% rename from frontend/src/app/pages/Dashboard/hooks/useWebviewLifecycle.ts rename to frontend/src/app/pages/Dashboard/BrowserCard/hooks/useWebviewLifecycle.ts index 5ba70e95..5383c5a5 100644 --- a/frontend/src/app/pages/Dashboard/hooks/useWebviewLifecycle.ts +++ b/frontend/src/app/pages/Dashboard/BrowserCard/hooks/useWebviewLifecycle.ts @@ -1,8 +1,8 @@ import { useRef, useEffect } from 'react'; import { useAppDispatch } from '@/shared/hooks'; import { - registerWebview, - unregisterWebview, + registerWebview, // only used in this file, maybe an aNr opportunity? -HD + unregisterWebview, // only used in this file, maybe an aNr opportunity? -HD setActiveTab as setRegistryActiveTab, type BrowserWebview, } from '@/shared/browserRegistry'; @@ -12,15 +12,10 @@ import { updateBrowserTabFavicon, type BrowserTab, } from '@/shared/state/dashboardLayoutSlice'; +import type { TabLocalState } from '@/app/pages/Dashboard/types/types'; export type WebviewElement = BrowserWebview; -export interface TabLocalState { - loading: boolean; - canGoBack: boolean; - canGoForward: boolean; -} - export const isElectron = navigator.userAgent.includes('Electron'); export const chromeUserAgent = navigator.userAgent diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx b/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx index c410783c..ffb2eb01 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { AnimatePresence } from 'framer-motion'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import AgentCard from './AgentCard'; +import AgentCard from './AgentCard/AgentCard'; import DashboardViewCard from './DashboardViewCard'; -import BrowserCard from './BrowserCard'; +import BrowserCard from './BrowserCard/BrowserCard'; import CanvasControls from './CanvasControls'; import DashboardToolbar from './DashboardToolbar'; import DashboardHeader from './DashboardHeader'; diff --git a/frontend/src/app/pages/Dashboard/browserAgentOverlayUtils.ts b/frontend/src/app/pages/Dashboard/browserAgentOverlayUtils.ts deleted file mode 100644 index 5332646b..00000000 --- a/frontend/src/app/pages/Dashboard/browserAgentOverlayUtils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AgentMessage } from '@/shared/state/agentsSlice'; - -export interface OverlayEntry { - type: 'thought' | 'action' | 'result' | 'skip'; - text: string; -} - -export function summarizeMessage(msg: AgentMessage): OverlayEntry { - if (msg.role === 'assistant' && typeof msg.content === 'string') { - const trimmed = msg.content.trim(); - if (!trimmed) return { type: 'skip', text: '' }; - return { type: 'thought', text: trimmed }; - } - - if (msg.role === 'tool_call') { - const content = typeof msg.content === 'string' - ? (() => { try { return JSON.parse(msg.content); } catch { return {}; } })() - : msg.content; - const tool = content?.tool || content?.name || '?'; - const input = content?.input || {}; - let brief = ''; - switch (tool) { - case 'BrowserNavigate': brief = `Navigate → ${input.url || '...'}`; break; - case 'BrowserClick': brief = `Click ${input.selector || '...'}`; break; - case 'BrowserType': - brief = `Type "${(input.text || '').slice(0, 30)}${(input.text || '').length > 30 ? '…' : ''}" into ${input.selector || '...'}`; - break; - case 'BrowserScreenshot': brief = 'Screenshot'; break; - case 'BrowserGetText': brief = 'Read page text'; break; - case 'BrowserGetElements': - brief = `Inspect elements${input.selector ? ` (${input.selector})` : ''}`; - break; - case 'BrowserEvaluate': brief = 'Evaluate JS'; break; - default: brief = tool; - } - return { type: 'action', text: brief }; - } - - if (msg.role === 'tool_result') { - return { type: 'result', text: '' }; - } - - return { type: 'skip', text: '' }; -} diff --git a/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts b/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts index 737f2bd6..72084ef4 100644 --- a/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts +++ b/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts @@ -101,7 +101,7 @@ export function useDashboardInit(deps: InitDeps) { if (!layoutInitialized) return; const dashboardSessionIds = Object.values(sessions) .filter((s: any) => s.dashboard_id === dashboardId && s.mode !== 'browser-agent' && s.mode !== 'invoked-agent' && s.mode !== 'sub-agent') - .map((s: any) => s.id); + .map((s: any) => s.session_id); const liveIds = dashboardSessionIds.sort().join(','); if (liveIds === prevSessionIdsRef.current) return; prevSessionIdsRef.current = liveIds; diff --git a/frontend/src/app/pages/Dashboard/types/types.ts b/frontend/src/app/pages/Dashboard/types/types.ts new file mode 100644 index 00000000..60e9f51b --- /dev/null +++ b/frontend/src/app/pages/Dashboard/types/types.ts @@ -0,0 +1,6 @@ +export interface TabLocalState { + loading: boolean; + canGoBack: boolean; + canGoForward: boolean; + } + \ No newline at end of file diff --git a/frontend/src/shared/backend-bridge/apps/agents.ts b/frontend/src/shared/backend-bridge/apps/agents.ts index fbb1675d..1e1fb758 100644 --- a/frontend/src/shared/backend-bridge/apps/agents.ts +++ b/frontend/src/shared/backend-bridge/apps/agents.ts @@ -15,13 +15,12 @@ export const AGENTS_WS_API: string = `${API_BASE}/agents/ws`; const get_all_sessions_endpoint: string = `${AGENTS_API}/get_all_sessions`; async function get_all_sessions_function(dashboardId?: string): Promise { - const res = await fetch(get_all_sessions_endpoint, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ dashboard_id: dashboardId ?? '' }), - }); + const url = dashboardId + ? `${get_all_sessions_endpoint}?dashboard_id=${encodeURIComponent(dashboardId)}` + : get_all_sessions_endpoint; + const res = await fetch(url); const data = await res.json(); - return data.SESSIONS as AgentSession[]; + return data.sessions as AgentSession[]; } export const GET_ALL_SESSIONS = createAsyncThunk( get_all_sessions_endpoint, @@ -53,6 +52,7 @@ async function launch_agent_function(config: { mode: string; system_prompt: string; max_turns: number; + dashboard_id?: string; }): Promise<{ session_id: string; session: AgentSession }> { const res = await fetch(launch_agent_endpoint, { method: 'POST', @@ -62,6 +62,7 @@ async function launch_agent_function(config: { mode: config.mode, system_prompt: config.system_prompt, max_turns: config.max_turns, + dashboard_id: config.dashboard_id, }), }); const data = await res.json(); @@ -349,6 +350,7 @@ async function meta_launch_and_send_function( mode: payload.mode, system_prompt: payload.config.system_prompt ?? '', max_turns: payload.config.max_turns ?? 100, + dashboard_id: payload.config.dashboard_id, }); console.log(`[FRONTEND] meta_launch_and_send: launched | draftId=${payload.draftId} → realId=${session.session_id} status=${session.status} dashboard_id=${session.dashboard_id ?? 'NONE'}`); diff --git a/run/local.sh b/run/local.sh index 3c281b6b..c8aec1c1 100755 --- a/run/local.sh +++ b/run/local.sh @@ -40,12 +40,15 @@ cleanup() { echo "" echo -e "${YELLOW}${BOLD}Gracefully shutting down all services...${RESET}" - for pid in $ELECTRON_PID $BACKEND_PID $FRONTEND_PID; do + # Send SIGTERM to top-level PIDs only (not recursively) so uvicorn + # can run its lifespan teardown before child processes are killed. + for pid in $ELECTRON_PID $FRONTEND_PID; do [[ -n "$pid" ]] && kill_tree "$pid" TERM done + [[ -n "$BACKEND_PID" ]] && kill -TERM "$BACKEND_PID" 2>/dev/null local elapsed=0 - while (( elapsed < 5 )); do + while (( elapsed < 10 )); do local alive=false for pid in $ELECTRON_PID $BACKEND_PID $FRONTEND_PID; do [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && alive=true