mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
[Haik]: agentic refactor 7. Integration, SkillBuilderChat & Cleanup
This commit is contained in:
@@ -7,7 +7,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { motion } from 'framer-motion';
|
||||
import { parseMcpToolName, useMcpToolMeta, getToolIcon } from '@/app/pages/AgentChat/ApprovalBar';
|
||||
import { parseMcpToolName, useMcpToolMeta, getToolIcon } from '@/app/pages/AgentChat/toolkit/approval-tools';
|
||||
import { SPRING_BOUNCE } from './islandTypes';
|
||||
import type { ClaudeTokens } from './islandTypes';
|
||||
import type { ApprovalRequest } from '@/shared/state/agentsSlice';
|
||||
|
||||
@@ -4,7 +4,7 @@ import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { motion } from 'framer-motion';
|
||||
import ApprovalBar, { BatchApprovalBar } from '@/app/pages/AgentChat/ApprovalBar';
|
||||
import { ApprovalRouter as ApprovalBar, BatchApprovalWrapper as BatchApprovalBar } from '@/app/pages/AgentChat/toolkit/approval-tools';
|
||||
import { AgentStatusRow } from './AgentStatusRow';
|
||||
import { CompletedAgentsList } from './CompletedAgentsList';
|
||||
import type { ClaudeTokens, SessionApprovalGroup, TrackedAgent } from './islandTypes';
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { AssistantRuntimeProvider, useAui, Tools } from '@assistant-ui/react';
|
||||
import ApprovalBar, { BatchApprovalBar } from './ApprovalBar';
|
||||
import ChatInput from './ChatInput';
|
||||
import { ApprovalRouter, BatchApprovalWrapper } from './toolkit/approval-tools';
|
||||
import ChatHeader from './ChatHeader';
|
||||
import MessageQueue from './MessageQueue';
|
||||
import OpenSwarmThread from './thread/OpenSwarmThread';
|
||||
import OpenSwarmComposer from './composer/OpenSwarmComposer';
|
||||
import { useAgentChat } from './hooks/useAgentChat';
|
||||
import { useOpenSwarmRuntime } from './runtime/useOpenSwarmRuntime';
|
||||
import { useOpenSwarmRuntime, type ComposerExtras, type DispatchableMessage } from './runtime/useOpenSwarmRuntime';
|
||||
import { toolkit } from './toolkit';
|
||||
import { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
interface AgentChatProps {
|
||||
@@ -27,17 +27,22 @@ interface AgentChatProps {
|
||||
const AgentChat: React.FC<AgentChatProps> = ({ sessionId, onClose, embedded, autoFocus, isGlowing, onDismissGlow, initialContextPaths, onBranch }) => {
|
||||
const c = useClaudeTokens();
|
||||
const {
|
||||
id, session, isDraft, dispatch, mode, model,
|
||||
scrollContainerRef, chatInputRef, messageQueueRef,
|
||||
showScrollButton, showResumeBubble, awaitingResponse, editingMessageId,
|
||||
id, session, isDraft, mode, model,
|
||||
messageQueueRef, showResumeBubble,
|
||||
queueLength, setQueueLength, agentBusy,
|
||||
handleScroll, scrollToBottom, handleSend,
|
||||
handleModeChange, handleModelChange,
|
||||
handleSend, handleModeChange, handleModelChange,
|
||||
handleApprove, handleDeny, handleStop, handleResume,
|
||||
handleSaveEdit, handleCancelEdit, setEditingMessageId,
|
||||
} = useAgentChat({ sessionId, initialContextPaths });
|
||||
} = useAgentChat({ sessionId });
|
||||
|
||||
const runtime = useOpenSwarmRuntime(id);
|
||||
const composerExtrasRef = useRef<ComposerExtras>({});
|
||||
const dispatchForRuntime = useCallback((msg: DispatchableMessage) => {
|
||||
handleSend(msg.prompt, msg.images, msg.contextPaths, msg.forcedTools, msg.attachedSkills, msg.selectedBrowserIds);
|
||||
}, [handleSend]);
|
||||
|
||||
const runtime = useOpenSwarmRuntime(id, {
|
||||
composerExtrasRef,
|
||||
dispatchMessage: dispatchForRuntime,
|
||||
});
|
||||
const aui = useAui({ tools: Tools({ toolkit }) });
|
||||
|
||||
const contextEstimate = { used: 0, limit: 200_000 };
|
||||
@@ -57,13 +62,31 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId, onClose, embedded, aut
|
||||
<Box sx={{ flex: 1, minHeight: 0 }}>
|
||||
<OpenSwarmThread sessionId={id} onBranchChat={onBranch} />
|
||||
</Box>
|
||||
|
||||
{session.pending_approvals.length > 1 ? (
|
||||
<BatchApprovalBar requests={session.pending_approvals} onApprove={handleApprove} onDeny={handleDeny} />
|
||||
<BatchApprovalWrapper requests={session.pending_approvals} onApprove={handleApprove} onDeny={handleDeny} />
|
||||
) : (
|
||||
session.pending_approvals.map((req) => (
|
||||
<ApprovalBar key={req.id} request={req} onApprove={handleApprove} onDeny={handleDeny} />
|
||||
<ApprovalRouter key={req.id} request={req} onApprove={handleApprove} onDeny={handleDeny} />
|
||||
))
|
||||
)}
|
||||
|
||||
{showResumeBubble && session.status === 'stopped' && (
|
||||
<Box
|
||||
onClick={handleResume}
|
||||
sx={{
|
||||
mx: 1.5, mb: 1.5, py: 1.25, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
borderRadius: 2.5, cursor: 'pointer', fontWeight: 600, fontSize: '0.85rem',
|
||||
color: c.accent.primary, border: `1.5px solid ${c.accent.primary}`,
|
||||
background: `${c.accent.primary}08`,
|
||||
transition: 'background 0.15s, box-shadow 0.15s',
|
||||
'&:hover': { background: `${c.accent.primary}14` },
|
||||
}}
|
||||
>
|
||||
Resume
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isGlowing ? (
|
||||
<Box
|
||||
onClick={(e) => { e.stopPropagation(); onDismissGlow?.(); }}
|
||||
@@ -86,10 +109,8 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId, onClose, embedded, aut
|
||||
</Box>
|
||||
) : (
|
||||
<MessageQueue messageQueueRef={messageQueueRef} queueLength={queueLength} setQueueLength={setQueueLength}>
|
||||
<ChatInput
|
||||
ref={chatInputRef}
|
||||
onSend={handleSend}
|
||||
disabled={false}
|
||||
<OpenSwarmComposer
|
||||
composerExtrasRef={composerExtrasRef}
|
||||
mode={mode}
|
||||
onModeChange={handleModeChange}
|
||||
model={model}
|
||||
@@ -100,6 +121,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId, onClose, embedded, aut
|
||||
contextEstimate={contextEstimate}
|
||||
sessionId={id}
|
||||
autoFocus={autoFocus}
|
||||
initialContextPaths={initialContextPaths}
|
||||
/>
|
||||
</MessageQueue>
|
||||
)}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Re-export stub — logic moved to toolkit/approval-tools.tsx.
|
||||
* Kept for backward compatibility with AgentChat, DynamicIsland, and Dashboard imports.
|
||||
*/
|
||||
export { ApprovalRouter as default } from './toolkit/approval-tools';
|
||||
export { ToolQuestion as QuestionForm } from './toolkit/approval-tools';
|
||||
export type { ToolQuestionProps as QuestionFormProps } from './toolkit/approval-tools';
|
||||
export { BatchApprovalWrapper as BatchApprovalBar } from './toolkit/approval-tools';
|
||||
export { parseMcpToolName, useMcpToolMeta, getToolIcon } from './toolkit/approval-tools';
|
||||
export type { ParsedTool } from './toolkit/approval-tools';
|
||||
@@ -1,183 +0,0 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import AdsClickIcon from '@mui/icons-material/AdsClick';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import PsychologyOutlinedIcon from '@mui/icons-material/PsychologyOutlined';
|
||||
import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined';
|
||||
import { AgentMessage } from '@/shared/state/agentsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { SKILL_COLOR } from '@/app/components/richEditorUtils';
|
||||
|
||||
interface ParsedElement {
|
||||
label: string;
|
||||
selector: string;
|
||||
isSemantic?: boolean;
|
||||
}
|
||||
|
||||
interface ContextGroup {
|
||||
key: string;
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
label: string;
|
||||
chips: Array<{ label: string; tooltip?: string; icon: React.ReactNode }>;
|
||||
}
|
||||
|
||||
function buildContextGroups(
|
||||
elements: ParsedElement[],
|
||||
message: AgentMessage,
|
||||
): ContextGroup[] {
|
||||
const groups: ContextGroup[] = [];
|
||||
|
||||
if (elements.length > 0) {
|
||||
groups.push({
|
||||
key: 'elements',
|
||||
icon: <AdsClickIcon sx={{ fontSize: 13 }} />,
|
||||
color: '#3b82f6',
|
||||
label: `${elements.length} element${elements.length > 1 ? 's' : ''} selected`,
|
||||
chips: elements.map((el) => ({
|
||||
label: el.label,
|
||||
tooltip: el.selector,
|
||||
icon: <AdsClickIcon sx={{ fontSize: 12 }} />,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
const contextPaths = message.context_paths;
|
||||
if (contextPaths && contextPaths.length > 0) {
|
||||
const files = contextPaths.filter((cp) => cp.type === 'file');
|
||||
const dirs = contextPaths.filter((cp) => cp.type === 'directory');
|
||||
const allPaths = [...dirs, ...files];
|
||||
const label = [
|
||||
dirs.length > 0 ? `${dirs.length} folder${dirs.length > 1 ? 's' : ''}` : '',
|
||||
files.length > 0 ? `${files.length} file${files.length > 1 ? 's' : ''}` : '',
|
||||
].filter(Boolean).join(', ') + ' attached';
|
||||
groups.push({
|
||||
key: 'paths',
|
||||
icon: <FolderOutlinedIcon sx={{ fontSize: 13 }} />,
|
||||
color: '#10b981',
|
||||
label,
|
||||
chips: allPaths.map((cp) => {
|
||||
const name = cp.path.split('/').filter(Boolean).pop() || cp.path;
|
||||
return {
|
||||
label: name,
|
||||
tooltip: cp.path,
|
||||
icon: cp.type === 'directory'
|
||||
? <FolderOutlinedIcon sx={{ fontSize: 12 }} />
|
||||
: <InsertDriveFileOutlinedIcon sx={{ fontSize: 12 }} />,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const skills = message.attached_skills;
|
||||
if (skills && skills.length > 0) {
|
||||
groups.push({
|
||||
key: 'skills',
|
||||
icon: <PsychologyOutlinedIcon sx={{ fontSize: 13 }} />,
|
||||
color: SKILL_COLOR,
|
||||
label: `${skills.length} skill${skills.length > 1 ? 's' : ''}`,
|
||||
chips: skills.map((s) => ({
|
||||
label: s.name,
|
||||
icon: <PsychologyOutlinedIcon sx={{ fontSize: 12 }} />,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
const forcedTools = message.forced_tools;
|
||||
if (forcedTools && forcedTools.length > 0) {
|
||||
groups.push({
|
||||
key: 'tools',
|
||||
icon: <BuildOutlinedIcon sx={{ fontSize: 13 }} />,
|
||||
color: '#f59e0b',
|
||||
label: `${forcedTools.length} action${forcedTools.length > 1 ? 's' : ''} requested`,
|
||||
chips: forcedTools.map((t) => ({
|
||||
label: t,
|
||||
icon: <BuildOutlinedIcon sx={{ fontSize: 12 }} />,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
const AttachedContextSection: React.FC<{
|
||||
elements: ParsedElement[];
|
||||
message: AgentMessage;
|
||||
c: ReturnType<typeof useClaudeTokens>;
|
||||
}> = ({ elements, message, c }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const groups = useMemo(() => buildContextGroups(elements, message), [elements, message]);
|
||||
|
||||
if (groups.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 1, pt: 0.75, borderTop: `1px solid ${c.border.subtle}` }}>
|
||||
<Box
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
cursor: 'pointer',
|
||||
mb: 0.5,
|
||||
'&:hover': { opacity: 0.8 },
|
||||
}}
|
||||
>
|
||||
{groups.map((g) => (
|
||||
<Box key={g.key} sx={{ color: g.color, display: 'inline-flex', alignItems: 'center' }}>
|
||||
{g.icon}
|
||||
</Box>
|
||||
))}
|
||||
<Typography sx={{ fontSize: '0.7rem', fontWeight: 600, color: c.text.muted }}>
|
||||
{groups.map((g) => g.label).join(' · ')}
|
||||
</Typography>
|
||||
<ExpandMoreIcon
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
color: c.text.tertiary,
|
||||
transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
transition: '0.15s',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Collapse in={expanded}>
|
||||
{groups.map((g) => (
|
||||
<Box key={g.key} sx={{ mt: 0.5 }}>
|
||||
<Typography sx={{ fontSize: '0.62rem', fontWeight: 600, color: g.color, textTransform: 'uppercase', letterSpacing: 0.5, mb: 0.25 }}>
|
||||
{g.label}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{g.chips.map((chip, i) => (
|
||||
<Tooltip key={i} title={chip.tooltip || chip.label} arrow placement="top"
|
||||
slotProps={{ tooltip: { sx: { fontFamily: c.font.mono, fontSize: '0.68rem', maxWidth: 400 } } }}
|
||||
>
|
||||
<Chip
|
||||
icon={chip.icon as React.ReactElement}
|
||||
label={chip.label}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: `${g.color}18`,
|
||||
color: g.color,
|
||||
fontSize: '0.68rem',
|
||||
fontFamily: c.font.mono,
|
||||
height: 22,
|
||||
'& .MuiChip-icon': { color: g.color },
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachedContextSection;
|
||||
@@ -1,200 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import { AgentMessage, ToolGroupMeta } from '@/shared/state/agentsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { sanitizeSvgString } from '@/shared/sanitizeSvg';
|
||||
import ToolCallBubble, { ToolPair } from './ToolCallBubble';
|
||||
|
||||
export interface ToolGroup {
|
||||
type: 'tool_group';
|
||||
id: string;
|
||||
pairs: ToolPair[];
|
||||
label: string;
|
||||
callCount: number;
|
||||
mcpServer?: string;
|
||||
}
|
||||
|
||||
export type RenderItem = AgentMessage | ToolGroup | ToolPair;
|
||||
|
||||
export function isToolGroup(item: RenderItem): item is ToolGroup {
|
||||
return (item as ToolGroup).type === 'tool_group';
|
||||
}
|
||||
|
||||
export function isToolPair(item: RenderItem): item is ToolPair {
|
||||
return (item as ToolPair).type === 'tool_pair';
|
||||
}
|
||||
|
||||
const GeneratedSvgIcon: React.FC<{ svg: string; size?: number; color: string }> = ({ svg, size = 16, color }) => {
|
||||
const sanitized = useMemo(() => sanitizeSvgString(svg), [svg]);
|
||||
if (!sanitized) return null;
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
style={{ flexShrink: 0, color }}
|
||||
dangerouslySetInnerHTML={{ __html: sanitized }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SkeletonPulse: React.FC<{ width: number; height: number; borderRadius?: number }> = ({ width, height, borderRadius = 4 }) => (
|
||||
<Box
|
||||
sx={{
|
||||
width,
|
||||
height,
|
||||
borderRadius: `${borderRadius}px`,
|
||||
bgcolor: 'currentColor',
|
||||
opacity: 0.1,
|
||||
animation: 'pulse 1.5s ease-in-out infinite',
|
||||
'@keyframes pulse': {
|
||||
'0%, 100%': { opacity: 0.1 },
|
||||
'50%': { opacity: 0.2 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
interface Props {
|
||||
group: ToolGroup;
|
||||
isSessionRunning?: boolean;
|
||||
meta?: ToolGroupMeta;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
const ToolGroupBubble: React.FC<Props> = React.memo(({ group, isSessionRunning = false, meta, sessionId }) => {
|
||||
const c = useClaudeTokens();
|
||||
const isMcp = !!group.mcpServer;
|
||||
const [expanded, setExpanded] = useState(isMcp);
|
||||
|
||||
const completedCount = group.pairs.filter((p) => p.result !== null).length;
|
||||
const pendingCount = group.pairs.filter((p) => p.result === null).length;
|
||||
const deniedCount = group.pairs.filter(
|
||||
(p) => typeof p.call.content === 'object' && p.call.content.approved === false
|
||||
).length;
|
||||
const allDone = pendingCount === 0 || !isSessionRunning;
|
||||
|
||||
const displayName = meta?.name || group.label;
|
||||
const hasSvg = !!meta?.svg;
|
||||
|
||||
const toolNames = group.pairs.map((p) => {
|
||||
const c2 = typeof p.call.content === 'object' ? p.call.content : {};
|
||||
return c2.tool || 'unknown';
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-select-type="tool-group"
|
||||
data-select-id={group.id}
|
||||
data-select-meta={JSON.stringify({ label: displayName, callCount: group.callCount, tools: toolNames })}
|
||||
sx={{ maxWidth: '85%', my: 0.5 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: c.bg.elevated,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.5,
|
||||
py: 0.7,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: 'rgba(0,0,0,0.02)' },
|
||||
}}
|
||||
>
|
||||
{!meta ? (
|
||||
<SkeletonPulse width={15} height={15} borderRadius={8} />
|
||||
) : hasSvg ? (
|
||||
<GeneratedSvgIcon svg={meta.svg} size={15} color={c.accent.primary} />
|
||||
) : (
|
||||
<TerminalIcon sx={{ fontSize: 15, color: c.accent.primary, flexShrink: 0 }} />
|
||||
)}
|
||||
|
||||
{!meta ? (
|
||||
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center' }}>
|
||||
<SkeletonPulse width={100} height={12} />
|
||||
</Box>
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.accent.primary,
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 600,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{displayName}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{deniedCount > 0 && (
|
||||
<Typography sx={{ color: c.status.error, fontSize: '0.68rem' }}>
|
||||
{deniedCount} denied
|
||||
</Typography>
|
||||
)}
|
||||
{allDone && completedCount > 0 && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.3 }}>
|
||||
<CheckCircleOutlineIcon sx={{ fontSize: 12, color: c.status.success }} />
|
||||
<Typography sx={{ color: c.status.success, fontSize: '0.68rem' }}>
|
||||
{completedCount}/{group.callCount}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{!allDone && pendingCount > 0 && (
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.68rem', fontFamily: c.font.mono }}>
|
||||
{completedCount}/{group.callCount}
|
||||
</Typography>
|
||||
)}
|
||||
<Chip
|
||||
label={`×${group.callCount}`}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 18,
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 600,
|
||||
bgcolor: c.bg.secondary,
|
||||
color: c.text.muted,
|
||||
'& .MuiChip-label': { px: 0.75 },
|
||||
}}
|
||||
/>
|
||||
<IconButton size="small" sx={{ color: c.text.tertiary, p: 0.15 }}>
|
||||
{expanded ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Collapse in={expanded}>
|
||||
<Box sx={{ borderTop: `0.5px solid ${c.border.medium}` }}>
|
||||
{group.pairs.map((pair) => (
|
||||
<ToolCallBubble
|
||||
key={pair.id}
|
||||
call={pair.call}
|
||||
result={pair.result}
|
||||
isPending={pair.result === null && isSessionRunning}
|
||||
mcpCompact
|
||||
sessionId={sessionId}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default ToolGroupBubble;
|
||||
@@ -5,6 +5,7 @@ import type { Unstable_MentionItem } from '@assistant-ui/core';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
import type { PromptTemplate } from '@/shared/state/templatesSlice';
|
||||
import type { ComposerExtras } from '../runtime/useOpenSwarmRuntime';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import { useOpenSwarmMentionAdapter, type MentionItemMetadata } from './OpenSwarmMentionAdapter';
|
||||
import { useComposerAttachments } from './useComposerAttachments';
|
||||
import { MentionSelectOverride, MentionPopover, ComposerAttachmentChips } from './ComposerParts';
|
||||
@@ -23,11 +24,13 @@ export interface OpenSwarmComposerProps {
|
||||
queueLength?: number;
|
||||
contextEstimate?: { used: number; limit: number };
|
||||
autoFocus?: boolean;
|
||||
initialContextPaths?: ContextPath[];
|
||||
}
|
||||
|
||||
const OpenSwarmComposer: FC<OpenSwarmComposerProps> = ({
|
||||
composerExtrasRef, mode, onModeChange, model, onModelChange,
|
||||
isRunning, onStop, sessionId, queueLength, contextEstimate, autoFocus,
|
||||
initialContextPaths,
|
||||
}) => {
|
||||
const aui = useAui();
|
||||
const mentionAdapter = useOpenSwarmMentionAdapter();
|
||||
@@ -36,9 +39,16 @@ const OpenSwarmComposer: FC<OpenSwarmComposerProps> = ({
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<PromptTemplate | null>(null);
|
||||
const [hasContent, setHasContent] = useState(false);
|
||||
|
||||
const initialContextApplied = useRef(false);
|
||||
const templates = useAppSelector((s) => s.templates.items);
|
||||
const skills = useAppSelector((s) => s.skills.items);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialContextApplied.current || !initialContextPaths?.length) return;
|
||||
att.setContextPaths(initialContextPaths);
|
||||
initialContextApplied.current = true;
|
||||
}, [initialContextPaths, att]);
|
||||
|
||||
const syncExtras = useCallback(() => {
|
||||
const allForcedTools = att.forcedTools.flatMap((ft) => ft.tools);
|
||||
const skillList = Object.values(att.attachedSkills);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
} from '@/shared/state/agentsSlice';
|
||||
import { fetchModes } from '@/shared/state/modesSlice';
|
||||
import { createSessionWs } from '@/shared/ws/WebSocketManager';
|
||||
import type { ChatInputHandle } from '../ChatInput';
|
||||
import type { ContextPath } from '@/app/components/DirectoryBrowser';
|
||||
import { setGlowingBrowserCards, fadeGlowingBrowserCards, clearGlowingBrowserCards } from '@/shared/state/dashboardLayoutSlice';
|
||||
|
||||
export interface QueuedMessage {
|
||||
@@ -29,25 +27,19 @@ export interface QueuedMessage {
|
||||
|
||||
interface UseAgentChatParams {
|
||||
sessionId?: string;
|
||||
initialContextPaths?: ContextPath[];
|
||||
}
|
||||
|
||||
export function useAgentChat({ sessionId: sessionIdProp, initialContextPaths }: UseAgentChatParams) {
|
||||
export function useAgentChat({ sessionId: sessionIdProp }: UseAgentChatParams) {
|
||||
const { id: routeId } = useParams<{ id: string }>();
|
||||
const id = sessionIdProp || routeId;
|
||||
const dispatch = useAppDispatch();
|
||||
const session = useAppSelector((state) => (id ? state.agents.sessions[id] : undefined));
|
||||
const modesMap = useAppSelector((state) => state.modes.items);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const chatInputRef = useRef<ChatInputHandle>(null);
|
||||
const isAtBottomRef = useRef(true);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const [showResumeBubble, setShowResumeBubble] = useState(false);
|
||||
const [awaitingResponse, setAwaitingResponse] = useState(false);
|
||||
const [mode, setMode] = useState('agent');
|
||||
const [model, setModel] = useState('sonnet');
|
||||
const wsRef = useRef<ReturnType<typeof createSessionWs> | null>(null);
|
||||
const initialContextApplied = useRef(false);
|
||||
const messageQueueRef = useRef<QueuedMessage[]>([]);
|
||||
const [queueLength, setQueueLength] = useState(0);
|
||||
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
||||
@@ -63,15 +55,6 @@ export function useAgentChat({ sessionId: sessionIdProp, initialContextPaths }:
|
||||
return () => { ws.disconnect(); wsRef.current = null; };
|
||||
}, [id, isDraft, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialContextApplied.current || !initialContextPaths?.length) return;
|
||||
const timer = setTimeout(() => {
|
||||
chatInputRef.current?.setContent('', initialContextPaths);
|
||||
initialContextApplied.current = true;
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}, [initialContextPaths]);
|
||||
|
||||
useEffect(() => { if (session) setMode(session.mode); }, [session?.mode]);
|
||||
useEffect(() => { if (session) setModel(session.model); }, [session?.model]);
|
||||
useEffect(() => { if (Object.keys(modesMap).length === 0) dispatch(fetchModes()); }, [dispatch, modesMap]);
|
||||
@@ -137,30 +120,6 @@ export function useAgentChat({ sessionId: sessionIdProp, initialContextPaths }:
|
||||
if (curr !== 'draft' && !didDispatchQueued) setAwaitingResponse(false);
|
||||
}, [session?.status, mode, modesMap, id, isDraft, dispatch, dispatchMessage]);
|
||||
|
||||
const SCROLL_THRESHOLD = 50;
|
||||
const handleScroll = useCallback(() => {
|
||||
const el = scrollContainerRef.current;
|
||||
if (!el) return;
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < SCROLL_THRESHOLD;
|
||||
isAtBottomRef.current = atBottom;
|
||||
setShowScrollButton(!atBottom);
|
||||
}, []);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
const el = scrollContainerRef.current;
|
||||
if (!el) return;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
isAtBottomRef.current = true;
|
||||
setShowScrollButton(false);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isAtBottomRef.current) {
|
||||
const el = scrollContainerRef.current;
|
||||
if (el) el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}, [session?.messages.length, session?.streamingMessage?.content]);
|
||||
|
||||
const handleSend = (prompt: string, images?: Array<{ data: string; media_type: string }>, contextPaths?: Array<{ path: string; type: 'file' | 'directory' }>, forcedTools?: string[], attachedSkills?: Array<{ id: string; name: string; content: string }>, selectedBrowserIds?: string[]) => {
|
||||
if (!id) return;
|
||||
const msg: QueuedMessage = { prompt, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds };
|
||||
@@ -211,11 +170,9 @@ export function useAgentChat({ sessionId: sessionIdProp, initialContextPaths }:
|
||||
|
||||
return {
|
||||
id, session, isDraft, dispatch, mode, model,
|
||||
scrollContainerRef, chatInputRef, messageQueueRef,
|
||||
showScrollButton, showResumeBubble, awaitingResponse, editingMessageId,
|
||||
messageQueueRef, showResumeBubble, awaitingResponse, editingMessageId,
|
||||
queueLength, setQueueLength, agentBusy,
|
||||
handleScroll, scrollToBottom, handleSend,
|
||||
handleModeChange, handleModelChange,
|
||||
handleSend, handleModeChange, handleModelChange,
|
||||
handleApprove, handleDeny, handleStop, handleResume,
|
||||
handleSaveEdit, handleCancelEdit, setEditingMessageId,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import { AgentSession, handleApproval } from '@/shared/state/agentsSlice';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { QuestionForm } from '@/app/pages/AgentChat/ApprovalBar';
|
||||
import { ToolQuestion as QuestionForm } from '@/app/pages/AgentChat/toolkit/approval-tools';
|
||||
import { parseMcpToolName } from '@/app/pages/AgentChat/ToolCallBubble';
|
||||
import GoogleServiceIcon from '@/app/components/GoogleServiceIcon';
|
||||
import { summarizeToolInput, getToolDisplayName } from './agentCardUtils';
|
||||
|
||||
Reference in New Issue
Block a user