diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index 58190598..d13f0e3e 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -118,7 +118,7 @@ async def get_all_sessions(dashboard_id: str = Body(default="")) -> dict: result: List[Agent] = list[Agent](SESSIONS.values()) if dashboard_id: result: List[Agent] = [a for a in result if getattr(a, "dashboard_id", None) == dashboard_id] - return {"SESSIONS": [a.model_dump(mode="json") for a in result]} + return {"sessions": [a.model_dump(mode="json") for a in result]} @agents.router.get("/get_session") diff --git a/frontend/src/app/pages/AgentChat/hooks/useAgentChat.ts b/frontend/src/app/pages/AgentChat/hooks/useAgentChat.ts index afdf9eed..35da5d25 100644 --- a/frontend/src/app/pages/AgentChat/hooks/useAgentChat.ts +++ b/frontend/src/app/pages/AgentChat/hooks/useAgentChat.ts @@ -1,23 +1,14 @@ import { useEffect, useRef, useState, useCallback } from 'react'; import { useParams } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -// import { -// sendMessage as sendMessageThunk, -// launchAndSendFirstMessage, -// generateTitle, -// stopAgent, -// handleApproval, -// editMessage, -// updateSessionModel, -// updateSessionMode, -// fetchSession, -// } from '@/shared/state/agentsSlice'; +import type { AgentConfig } from '@/shared/state/agentsTypes'; import { SEND_MESSAGE, STOP_AGENT, HANDLE_APPROVAL, EDIT_MESSAGE, GET_SESSION, + META_LAUNCH_AND_SEND } from '@/shared/backend-bridge/apps/agents'; import { updateSessionMode, updateSessionModel } from '@/shared/state/agentsSlice'; import { fetchModes } from '@/shared/state/modesSlice'; @@ -67,17 +58,41 @@ export function useAgentChat({ sessionId: sessionIdProp }: UseAgentChatParams) { setShowResumeBubble(false); setAwaitingResponse(true); if (isDraft) { - const config: Record = { model, mode }; + const config: AgentConfig = { + model: model, + mode: mode, + system_prompt: sessionSystemPrompt ?? undefined, + target_directory: sessionTargetDirectory ?? undefined, + }; if (sessionSystemPrompt) config.system_prompt = sessionSystemPrompt; if (sessionTargetDirectory) config.target_directory = sessionTargetDirectory; dispatch( - launchAndSendFirstMessage({ draftId: id, config, prompt: msg.prompt, mode, model, images: msg.images, contextPaths: msg.contextPaths, forcedTools: msg.forcedTools, attachedSkills: msg.attachedSkills, selectedBrowserIds: msg.selectedBrowserIds }) + META_LAUNCH_AND_SEND({ + draftId: id, + config, + prompt: msg.prompt, + mode, + model, + images: msg.images, + contextPaths: msg.contextPaths, + forcedTools: msg.forcedTools, + attachedSkills: msg.attachedSkills, + selectedBrowserIds: msg.selectedBrowserIds + }) ).then((action) => { - if (launchAndSendFirstMessage.fulfilled.match(action)) { + if (META_LAUNCH_AND_SEND.fulfilled.match(action)) { const realId = action.payload.session.id; - dispatch(generateTitle({ sessionId: realId, prompt: msg.prompt })); + // TODO: Implement title generation + // dispatch(generateTitle({ + // sessionId: realId, + // prompt: msg.prompt + // })); if (msg.selectedBrowserIds?.length) { - dispatch(setGlowingBrowserCards({ browserIds: msg.selectedBrowserIds, sessionId: realId, label: 'Use Browser' })); + dispatch(setGlowingBrowserCards({ + browserIds: msg.selectedBrowserIds, + sessionId: realId, + label: 'Use Browser' + })); } } }); @@ -85,7 +100,19 @@ export function useAgentChat({ sessionId: sessionIdProp }: UseAgentChatParams) { if (msg.selectedBrowserIds?.length) { dispatch(setGlowingBrowserCards({ browserIds: msg.selectedBrowserIds, sessionId: id, label: 'Use Browser' })); } - dispatch(SEND_MESSAGE({ sessionId: id, prompt: msg.prompt, mode, model, images: msg.images, contextPaths: msg.contextPaths, forcedTools: msg.forcedTools, attachedSkills: msg.attachedSkills, selectedBrowserIds: msg.selectedBrowserIds })) + dispatch(SEND_MESSAGE({ + sessionId: id, + prompt: msg.prompt, + mode: mode, + model: model, + images: msg.images?.map((img) => img.data), + imageMediaTypes: msg.images?.map((img) => img.media_type), + contextPaths: msg.contextPaths, + forcedTools: msg.forcedTools, + attachedSkills: msg.attachedSkills, + // TODO: Implement the selectedBrowserIds below + // selectedBrowserIds: msg.selectedBrowserIds + })) .then((action) => { if (SEND_MESSAGE.rejected.match(action)) setAwaitingResponse(false); }); } }, [id, isDraft, mode, model, sessionSystemPrompt, sessionTargetDirectory, dispatch]); @@ -141,7 +168,7 @@ export function useAgentChat({ sessionId: sessionIdProp }: UseAgentChatParams) { if (id && !isDraft) dispatch(updateSessionModel({ sessionId: id, model: newModel })); }, [id, isDraft, dispatch]); - const handleApprove = (requestId: string, updatedInput?: Record) => { + const handleApprove = (requestId: string, updatedInput?: Record) => { dispatch(HANDLE_APPROVAL({ requestId, behavior: 'allow', updatedInput })); }; const handleDeny = (requestId: string, message?: string) => { diff --git a/frontend/src/app/pages/Dashboard/hooks/useToolbarActions.ts b/frontend/src/app/pages/Dashboard/hooks/useToolbarActions.ts index e9410a8d..d7ab9c24 100644 --- a/frontend/src/app/pages/Dashboard/hooks/useToolbarActions.ts +++ b/frontend/src/app/pages/Dashboard/hooks/useToolbarActions.ts @@ -2,14 +2,8 @@ import { useCallback } from 'react'; import type { RefObject, MutableRefObject } from 'react'; import { useAppDispatch } from '@/shared/hooks'; import { store } from '@/shared/state/store'; -// import { -// launchAndSendFirstMessage, -// generateTitle, -// expandSession, -// resumeSession, -// } from '@/shared/state/agentsSlice'; - -import { RESUME_SESSION } from '@/shared/backend-bridge/apps/agents'; +import type { AgentSession } from '@/shared/state/agentsTypes'; +import { RESUME_SESSION, META_LAUNCH_AND_SEND } from '@/shared/backend-bridge/apps/agents'; import { expandSession } from '@/shared/state/agentsSlice'; import type { AgentConfig } from '@/shared/state/agentsSlice'; import { @@ -24,13 +18,16 @@ import { DEFAULT_CARD_H, EXPANDED_CARD_MIN_H, GRID_GAP, + CardPosition, + ViewCardPosition, + BrowserCardPosition } from '@/shared/state/dashboardLayoutSlice'; import { generateDashboardName } from '@/shared/state/dashboardsSlice'; import type { ContextPath } from '@/app/components/DirectoryBrowser'; import type { CanvasActions } from '../useCanvasControls'; interface ToolbarDeps { - cards: Record; + cards: Record; expandedSessionIds: string[]; viewportRef: RefObject; canvasActions: CanvasActions; @@ -62,11 +59,11 @@ export function useToolbarActions(deps: ToolbarDeps) { const targetX = sourceCard.x + sourceCard.width + GRID_GAP * 12; let targetY = sourceCard.y; const columnCards = Object.values(cards).filter( - (c: any) => Math.abs(c.x - targetX) < 50 && c.session_id !== newSessionId, + (c: CardPosition) => Math.abs(c.x - targetX) < 50 && c.session_id !== newSessionId, ); if (columnCards.length > 0) { const lowestBottom = Math.max( - ...(columnCards as any[]).map((c) => c.y + Math.max(EXPANDED_CARD_MIN_H, c.height)), + ...columnCards.map((c) => c.y + Math.max(EXPANDED_CARD_MIN_H, c.height)), ); targetY = lowestBottom + GRID_GAP; } @@ -106,15 +103,16 @@ export function useToolbarActions(deps: ToolbarDeps) { } const config: AgentConfig = { name: 'New chat', model, mode, dashboard_id: dashboardId }; dispatch( - launchAndSendFirstMessage({ + META_LAUNCH_AND_SEND({ draftId, config, prompt, mode, model, images, contextPaths: contextPaths?.map((cp) => ({ path: cp.path, type: cp.type })), forcedTools, attachedSkills, expand: expandNewChats, }), ).then((action) => { - if (launchAndSendFirstMessage.fulfilled.match(action)) { + if (META_LAUNCH_AND_SEND.fulfilled.match(action)) { const realId = action.payload.session.id; - dispatch(generateTitle({ sessionId: realId, prompt })); + // TODO: Implement title generation + // dispatch(generateTitle({ sessionId: realId, prompt })); if (selectedBrowserIds?.length) { dispatch(setGlowingBrowserCards({ browserIds: selectedBrowserIds, sessionId: realId, label: 'Use Browser' })); if (selectedBrowserIds.length === 1) { @@ -144,7 +142,7 @@ export function useToolbarActions(deps: ToolbarDeps) { if (dashboardId) { const currentSessions = store.getState().agents.sessions; const agentCount = Object.values(currentSessions).filter( - (s: any) => s.status !== 'draft' && s.dashboard_id === dashboardId, + (s: AgentSession) => s.status !== 'draft' && s.dashboard_id === dashboardId, ).length; const NAME_GEN_TRIGGERS = [1, 3, 6]; const currentDash = store.getState().dashboards.items[dashboardId]; @@ -212,12 +210,12 @@ export function useToolbarActions(deps: ToolbarDeps) { const expandedSet = new Set(currentExpanded); const { cards: tidied, viewCards: tidiedViews, browserCards: tidiedBrowsers } = store.getState().dashboardLayout; const allRects = [ - ...Object.values(tidied).map((c: any) => ({ + ...Object.values(tidied).map((c: CardPosition) => ({ x: c.x, y: c.y, width: c.width, height: expandedSet.has(c.session_id) ? Math.max(EXPANDED_CARD_MIN_H, c.height) : c.height, })), - ...Object.values(tidiedViews).map((c: any) => ({ x: c.x, y: c.y, width: c.width, height: c.height })), - ...Object.values(tidiedBrowsers).map((c: any) => ({ x: c.x, y: c.y, width: c.width, height: c.height })), + ...Object.values(tidiedViews).map((c: ViewCardPosition) => ({ x: c.x, y: c.y, width: c.width, height: c.height })), + ...Object.values(tidiedBrowsers).map((c: BrowserCardPosition) => ({ x: c.x, y: c.y, width: c.width, height: c.height })), ]; canvasActions.fitToCards(allRects); }, [dispatch, canvasActions]); diff --git a/frontend/src/app/pages/Dashboard/useDashboardToolbar.ts b/frontend/src/app/pages/Dashboard/useDashboardToolbar.ts index 5d6f158c..b39d3f14 100644 --- a/frontend/src/app/pages/Dashboard/useDashboardToolbar.ts +++ b/frontend/src/app/pages/Dashboard/useDashboardToolbar.ts @@ -3,7 +3,8 @@ import type { ContextPath } from '@/app/components/DirectoryBrowser'; import { useElementSelection } from '@/app/components/ElementSelectionContext'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { searchHistory, clearHistorySearch } from '@/shared/state/agentsSlice'; +import { clearHistorySearch } from '@/shared/state/agentsSlice'; +import { GET_HISTORY } from '@/shared/backend-bridge/apps/agents'; import type { Output } from '@/shared/state/outputsSlice'; import type { Props } from './toolbarShared'; import { TOOLBAR_OWNER_ID, HISTORY_PAGE_SIZE } from './toolbarShared'; @@ -22,14 +23,6 @@ export function useDashboardToolbar({ const defaultModel = useAppSelector((s) => s.settings.data.default_model); const [mode, setMode] = useState(defaultMode || 'agent'); const [model, setModel] = useState(defaultModel || 'sonnet'); - const settingsApplied = useRef(false); - useEffect(() => { - if (!settingsApplied.current) { - setMode(defaultMode || 'agent'); - setModel(defaultModel || 'sonnet'); - settingsApplied.current = true; - } - }, [defaultMode, defaultModel]); const [viewPickerOpen, setViewPickerOpen] = useState(false); const [viewSearch, setViewSearch] = useState(''); const [historyOpen, setHistoryOpen] = useState(false); @@ -120,8 +113,8 @@ export function useDashboardToolbar({ setHistoryOpen(true); setHistoryQuery(''); dispatch(clearHistorySearch()); - dispatch(searchHistory({ q: '', limit: HISTORY_PAGE_SIZE, offset: 0, dashboardId })); - }, [historyOpen, dispatch, dashboardId]); + dispatch(GET_HISTORY({ q: '', limit: HISTORY_PAGE_SIZE, offset: 0 })); + }, [historyOpen, dispatch]); const handleHistorySelect = useCallback((sessionId: string) => { onHistoryResume(sessionId); @@ -130,13 +123,12 @@ export function useDashboardToolbar({ const handleHistoryLoadMore = useCallback(() => { if (historySearchState.loading || !historySearchState.hasMore) return; - dispatch(searchHistory({ + dispatch(GET_HISTORY({ q: historyQuery, limit: HISTORY_PAGE_SIZE, offset: historySearchState.results.length, - dashboardId, })); - }, [dispatch, historyQuery, historySearchState.loading, historySearchState.hasMore, historySearchState.results.length, dashboardId]); + }, [dispatch, historyQuery, historySearchState.loading, historySearchState.hasMore, historySearchState.results.length]); const isExpanded = inputOpen || viewPickerOpen || historyOpen; @@ -227,7 +219,7 @@ export function useDashboardToolbar({ useEffect(() => { if (!historyOpen) return; const timer = setTimeout(() => { - dispatch(searchHistory({ q: historyQuery, limit: HISTORY_PAGE_SIZE, offset: 0, dashboardId })); + dispatch(GET_HISTORY({ q: historyQuery, limit: HISTORY_PAGE_SIZE, offset: 0 })); }, 300); return () => clearTimeout(timer); }, [historyQuery, historyOpen, dispatch, dashboardId]); diff --git a/frontend/src/shared/backend-bridge/apps/agents.ts b/frontend/src/shared/backend-bridge/apps/agents.ts index 5699a503..3370128b 100644 --- a/frontend/src/shared/backend-bridge/apps/agents.ts +++ b/frontend/src/shared/backend-bridge/apps/agents.ts @@ -1,7 +1,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { API_BASE } from '@/shared/backend-bridge/base_routes'; import type { - AgentSession, HistorySession, + AgentSession, HistorySession, LaunchAndSendPayload } from '@/shared/state/agentsTypes'; const AGENTS_API: string = `${API_BASE}/agents`; @@ -327,4 +327,42 @@ async function get_history_function(payload: { export const GET_HISTORY = createAsyncThunk( get_history_endpoint, get_history_function, +); + + + +// --------------------------------------------------------------------------- +// Meta Functions (Not actual endpoints in the backend) +// --------------------------------------------------------------------------- + + + +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 }> { + 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, + }); + + await send_message_function({ + sessionId: session.id, + prompt: payload.prompt, + mode: payload.mode, + model: payload.model, + images: payload.images?.map((img) => img.data), + imageMediaTypes: payload.images?.map((img) => img.media_type), + contextPaths: payload.contextPaths, + forcedTools: payload.forcedTools, + attachedSkills: payload.attachedSkills, + }); + + return { draftId: payload.draftId, session }; +} +export const META_LAUNCH_AND_SEND = createAsyncThunk( + meta_launch_and_send_endpoint, + meta_launch_and_send_function, ); \ No newline at end of file diff --git a/frontend/src/shared/state/agentsExtraReducers.ts b/frontend/src/shared/state/agentsExtraReducers.ts index da75d39b..bb314c8d 100644 --- a/frontend/src/shared/state/agentsExtraReducers.ts +++ b/frontend/src/shared/state/agentsExtraReducers.ts @@ -1,12 +1,5 @@ import type { ActionReducerMapBuilder } from '@reduxjs/toolkit'; import type { AgentsState, HistorySession } from './agentsTypes'; -// import { -// fetchSessions, launchAgent, launchAndSendFirstMessage, generateTitle, -// generateGroupMeta, updateSystemPrompt, sendMessage, editMessage, -// stopAgent, handleApproval, switchBranch, duplicateSession, -// closeSession, deleteSession, fetchHistory, resumeSession, -// fetchSession, fetchBrowserAgentChildren, searchHistory, -// } from './agentsThunks'; import { GET_ALL_SESSIONS, LAUNCH_AGENT, @@ -22,6 +15,7 @@ import { GET_HISTORY, RESUME_SESSION, GET_SESSION, + META_LAUNCH_AND_SEND, } from '@/shared/backend-bridge/apps/agents'; export function buildExtraReducers(builder: ActionReducerMapBuilder) { @@ -70,20 +64,20 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder } }) // TODO: Re-implement this??? - // .addCase(launchAndSendFirstMessage.fulfilled, (state, action) => { - // const { draftId, session } = action.payload; - // const shouldExpand = action.meta.arg.expand !== false; - // 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); - // } - // if (!state.trackedNotificationIds.includes(session.id)) { - // state.trackedNotificationIds.push(session.id); - // } - // }) + .addCase(META_LAUNCH_AND_SEND.fulfilled, (state, action) => { + const { draftId, session } = action.payload; + const shouldExpand = action.meta.arg.expand !== false; + 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); + } + if (!state.trackedNotificationIds.includes(session.id)) { + state.trackedNotificationIds.push(session.id); + } + }) // TODO: Re-implement this??? // .addCase(generateTitle.fulfilled, (state, action) => { // const session = state.sessions[action.payload.sessionId]; @@ -201,22 +195,27 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder tool_group_meta: session.tool_group_meta ?? existing?.tool_group_meta ?? {}, }; }) - // .addCase(searchHistory.pending, (state) => { - // state.historySearch.loading = true; - // }) - // .addCase(searchHistory.fulfilled, (state, action) => { - // const { sessions, total, hasMore, query, offset } = action.payload; - // if (offset === 0) { - // state.historySearch.results = sessions; - // } else { - // state.historySearch.results = [...state.historySearch.results, ...sessions]; - // } - // state.historySearch.total = total; - // state.historySearch.hasMore = hasMore; - // state.historySearch.query = query; - // state.historySearch.loading = false; - // }) - // .addCase(searchHistory.rejected, (state) => { - // state.historySearch.loading = false; - // }); + .addCase(GET_HISTORY.pending, (state) => { + state.historySearch.loading = true; + }) + .addCase(GET_HISTORY.fulfilled, (state, action) => { + const { sessions, total, has_more } = action.payload; + const offset = action.meta.arg.offset ?? 0; + if (offset === 0) { + state.historySearch.results = sessions; + } else { + state.historySearch.results = [...state.historySearch.results, ...sessions]; + } + state.historySearch.total = total; + state.historySearch.hasMore = has_more; + state.historySearch.query = action.meta.arg.q ?? ''; + state.historySearch.loading = false; + + const history: Record = {}; + for (const s of sessions) history[s.id] = s; + state.history = offset === 0 ? history : { ...state.history, ...history }; + }) + .addCase(GET_HISTORY.rejected, (state) => { + state.historySearch.loading = false; + }) } diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index b5eab17f..ae1a97d5 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { initialState } from './dashboardLayoutTypes'; import { dashboardLayoutReducers } from './dashboardLayoutReducers'; import { fetchLayout } from './dashboardLayoutThunks'; -import { launchAndSendFirstMessage } from './agentsSlice'; +import { META_LAUNCH_AND_SEND } from '@/shared/backend-bridge/apps/agents'; const dashboardLayoutSlice = createSlice({ name: 'dashboardLayout', @@ -40,7 +40,7 @@ const dashboardLayoutSlice = createSlice({ state.loading = false; state.initialized = true; }) - .addCase(launchAndSendFirstMessage.fulfilled, (state, action) => { + .addCase(META_LAUNCH_AND_SEND.fulfilled, (state, action) => { const { draftId, session } = action.payload; const card = state.cards[draftId]; if (card) {