[Haik]: migrate launchAndSendFirstMessage and searchHistory from agentsSlice to backend-bridge thunks (META_LAUNCH_AND_SEND, GET_HISTORY); wire up new thunks in useAgentChat, useToolbarActions, useDashboardToolbar, agentsExtraReducers, and dashboardLayoutSlice; normalize backend response key from SESSIONS to sessions; remove dashboardId param from history search calls; replace any types with AgentSession, CardPosition, and AgentConfig; remove stale settingsApplied useEffect; split image payload into separate data and media_type arrays for SEND_MESSAGE

This commit is contained in:
haikdc
2026-04-18 04:04:44 -07:00
parent 1ba5f11591
commit c329c0f837
7 changed files with 148 additions and 94 deletions
+1 -1
View File
@@ -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")
@@ -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<string, any> = { 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<string, any>) => {
const handleApprove = (requestId: string, updatedInput?: Record<string, unknown>) => {
dispatch(HANDLE_APPROVAL({ requestId, behavior: 'allow', updatedInput }));
};
const handleDeny = (requestId: string, message?: string) => {
@@ -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<string, any>;
cards: Record<string, CardPosition>;
expandedSessionIds: string[];
viewportRef: RefObject<HTMLDivElement>;
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]);
@@ -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]);
@@ -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,
);
@@ -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<AgentsState>) {
@@ -70,20 +64,20 @@ export function buildExtraReducers(builder: ActionReducerMapBuilder<AgentsState>
}
})
// 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<AgentsState>
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<string, HistorySession> = {};
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;
})
}
@@ -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) {