From a8370d0cce24872e3150498b9290c9c15ca8d9ce Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 18 Apr 2026 16:44:07 -0700 Subject: [PATCH] [Haik]: refactor: break apart approval-tools.tsx, approval-question.tsx, and approval-utils.tsx into a modular approvalToolkit/ directory with ApprovalRouter, BatchApprovalWrapper, ToolApproval, ToolQuestion, getMcpInputSummary, and useMcpToolMeta as separate files; relocate tool-ui components (ApprovalCard, option-list, question-flow) into co-located component folders; inline getToolIcon into CompactActionablePill using Lucide icons; replace toolkit/index.ts with toolkit.ts; update all consumer imports across AgentChat, ExpandedCard, CompactActionablePill, and Dashboard AgentCard components --- .../DynamicIsland/CompactActionablePill.tsx | 18 +- .../components/DynamicIsland/ExpandedCard.tsx | 3 +- .../src/app/pages/AgentChat/AgentChat.tsx | 3 +- .../AgentChat/toolkit/approval-tools.tsx | 170 --------------- .../AgentChat/toolkit/approval-utils.tsx | 193 ------------------ .../approvalToolkit/ApprovalRouter.tsx | 17 ++ .../approvalToolkit/BatchApprovalWrapper.tsx | 73 +++++++ .../ApprovalCard/ApprovalCard.tsx | 0 .../ToolApproval}/ApprovalCard/README.md | 0 .../ToolApproval}/ApprovalCard/_adapter.tsx | 0 .../ToolApproval}/ApprovalCard/schema.ts | 0 .../components/ToolApproval/ToolApproval.tsx | 90 ++++++++ .../ToolQuestion/OptionList/OptionList.tsx} | 6 +- .../ToolQuestion/OptionList}/README.md | 0 .../ToolQuestion/OptionList}/_adapter.tsx | 0 .../ToolQuestion/OptionList}/schema.ts | 6 +- .../ToolQuestion/OptionList}/selection.ts | 0 .../QuestionFlow/QuestionFlow.tsx} | 0 .../ToolQuestion/QuestionFlow}/README.md | 0 .../ToolQuestion/QuestionFlow}/_adapter.tsx | 0 .../ToolQuestion/QuestionFlow}/schema.ts | 2 +- .../components/ToolQuestion/ToolQuestion.tsx} | 6 +- .../components/getMcpInputSummary.ts | 53 +++++ .../components/useMcpToolMeta.tsx | 63 ++++++ .../toolkit/approvalToolkit/utils.tsx | 43 ++++ .../src/app/pages/AgentChat/toolkit/index.ts | 10 - .../app/pages/AgentChat/toolkit/toolkit.ts | 11 + .../AgentCardCollapsed/AgentCardCollapsed.tsx | 4 +- .../components/getToolDisplayName.ts | 2 +- .../components/summarizeToolInput.ts | 2 +- .../AgentCard/components/agentCardUtils.ts | 2 +- 31 files changed, 385 insertions(+), 392 deletions(-) delete mode 100644 frontend/src/app/pages/AgentChat/toolkit/approval-tools.tsx delete mode 100644 frontend/src/app/pages/AgentChat/toolkit/approval-utils.tsx create mode 100644 frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/ApprovalRouter.tsx create mode 100644 frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/BatchApprovalWrapper.tsx rename frontend/src/{components/tool-ui => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval}/ApprovalCard/ApprovalCard.tsx (100%) rename frontend/src/{components/tool-ui => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval}/ApprovalCard/README.md (100%) rename frontend/src/{components/tool-ui => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval}/ApprovalCard/_adapter.tsx (100%) rename frontend/src/{components/tool-ui => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval}/ApprovalCard/schema.ts (100%) create mode 100644 frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ToolApproval.tsx rename frontend/src/{components/tool-ui/option-list/option-list.tsx => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/OptionList.tsx} (98%) rename frontend/src/{components/tool-ui/option-list => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList}/README.md (100%) rename frontend/src/{components/tool-ui/option-list => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList}/_adapter.tsx (100%) rename frontend/src/{components/tool-ui/option-list => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList}/schema.ts (95%) rename frontend/src/{components/tool-ui/option-list => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList}/selection.ts (100%) rename frontend/src/{components/tool-ui/question-flow/question-flow.tsx => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/QuestionFlow.tsx} (100%) rename frontend/src/{components/tool-ui/question-flow => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow}/README.md (100%) rename frontend/src/{components/tool-ui/question-flow => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow}/_adapter.tsx (100%) rename frontend/src/{components/tool-ui/question-flow => app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow}/schema.ts (96%) rename frontend/src/app/pages/AgentChat/toolkit/{approval-question.tsx => approvalToolkit/components/ToolQuestion/ToolQuestion.tsx} (96%) create mode 100644 frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/getMcpInputSummary.ts create mode 100644 frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/useMcpToolMeta.tsx create mode 100644 frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/utils.tsx delete mode 100644 frontend/src/app/pages/AgentChat/toolkit/index.ts create mode 100644 frontend/src/app/pages/AgentChat/toolkit/toolkit.ts diff --git a/frontend/src/app/components/DynamicIsland/CompactActionablePill.tsx b/frontend/src/app/components/DynamicIsland/CompactActionablePill.tsx index 8d6cca25..02d74afa 100644 --- a/frontend/src/app/components/DynamicIsland/CompactActionablePill.tsx +++ b/frontend/src/app/components/DynamicIsland/CompactActionablePill.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import type { ReactNode } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; @@ -7,11 +8,24 @@ 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, getToolIcon } from '@/app/pages/AgentChat/toolkit/approval-utils'; -import { useMcpToolMeta } from '@/app/pages/AgentChat/toolkit/approval-tools'; +import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approvalToolkit/utils'; +import { useMcpToolMeta } from '@/app/pages/AgentChat/toolkit/approvalToolkit/components/useMcpToolMeta'; import { SPRING_BOUNCE } from './islandTypes'; import type { ClaudeTokens } from './islandTypes'; import type { ApprovalRequest } from '@/shared/state/agentsSlice'; +import { Terminal, FileText, FilePen, Search, MessageCircleQuestion, Wrench } from 'lucide-react'; + +function getToolIcon(toolName: string): ReactNode { + const size = 16; + switch (toolName) { + case 'Bash': return ; + case 'Read': return ; + case 'Write': case 'Edit': return ; + case 'Grep': case 'Glob': return ; + case 'AskUserQuestion': return ; + default: return ; + } +} export const CompactActionablePill: React.FC<{ c: ClaudeTokens; diff --git a/frontend/src/app/components/DynamicIsland/ExpandedCard.tsx b/frontend/src/app/components/DynamicIsland/ExpandedCard.tsx index b35ba4b9..6fbc4dc6 100644 --- a/frontend/src/app/components/DynamicIsland/ExpandedCard.tsx +++ b/frontend/src/app/components/DynamicIsland/ExpandedCard.tsx @@ -4,7 +4,8 @@ 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 { ApprovalRouter, BatchApprovalWrapper } from '@/app/pages/AgentChat/toolkit/approval-tools'; +import { ApprovalRouter } from '@/app/pages/AgentChat/toolkit/approvalToolkit/ApprovalRouter'; +import { BatchApprovalWrapper } from '@/app/pages/AgentChat/toolkit/approvalToolkit/BatchApprovalWrapper'; import { AgentStatusRow } from './AgentStatusRow'; import { CompletedAgentsList } from './CompletedAgentsList'; import type { ClaudeTokens, SessionApprovalGroup, TrackedAgent } from './islandTypes'; diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index 1386c127..c7ae5743 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -2,7 +2,8 @@ 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 { ApprovalRouter, BatchApprovalWrapper } from './toolkit/approval-tools'; +import { ApprovalRouter } from './toolkit/approvalToolkit/ApprovalRouter'; +import { BatchApprovalWrapper } from './toolkit/approvalToolkit/BatchApprovalWrapper'; import ChatHeader from './ChatHeader'; import MessageQueue from './MessageQueue'; import OpenSwarmThread from './OpenSwarmThread/OpenSwarmThread'; diff --git a/frontend/src/app/pages/AgentChat/toolkit/approval-tools.tsx b/frontend/src/app/pages/AgentChat/toolkit/approval-tools.tsx deleted file mode 100644 index 46fbe4f9..00000000 --- a/frontend/src/app/pages/AgentChat/toolkit/approval-tools.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { useMemo, useCallback } from 'react'; -import type { Toolkit } from '@assistant-ui/react'; -import { ApprovalCard } from '@/components/tool-ui/ApprovalCard/ApprovalCard'; -import type { ApprovalRequest } from '@/shared/state/agentsSlice'; -import { useAppSelector } from '@/shared/hooks'; -import type { ToolDefinition } from '@/shared/state/toolsSlice'; -import { - parseMcpToolName, sanitizeServerName, getMcpInputSummary, - getToolIconName, buildMetadata, isDangerous, - INTEGRATION_META, - type ParsedTool, type McpToolMeta, -} from './approval-utils'; -import { ToolQuestion } from './approval-question'; - -// --------------------------------------------------------------------------- -// useMcpToolMeta (React hook — lives here alongside other component code) -// --------------------------------------------------------------------------- - -export function useMcpToolMeta(parsed: ParsedTool): McpToolMeta { - const toolItems = useAppSelector((s) => s.tools.items); - - return useMemo(() => { - if (!parsed.isMcp) { - return { integration: null, description: '', serverLabel: '' }; - } - - const toolDef: ToolDefinition | undefined = Object.values(toolItems).find( - (t) => t.mcp_config && Object.keys(t.mcp_config).length > 0 - && sanitizeServerName(t.name) === parsed.serverSlug, - ); - - if (!toolDef) { - return { integration: null, description: '', serverLabel: parsed.serverSlug }; - } - - const description = toolDef.tool_permissions?._tool_descriptions?.[parsed.actionName] || ''; - const integration = INTEGRATION_META[toolDef.name] || null; - const serverLabel = toolDef.name; - - return { integration, description, serverLabel }; - }, [parsed, toolItems]); -} - -// --------------------------------------------------------------------------- -// ToolApproval -// --------------------------------------------------------------------------- - -interface ToolApprovalProps { - request: ApprovalRequest; - onApprove: (requestId: string, updatedInput?: Record) => void; - onDeny: (requestId: string, message?: string) => void; -} - -const ToolApproval: React.FC = ({ request, onApprove, onDeny }) => { - const parsed = useMemo(() => parseMcpToolName(request.tool_name), [request.tool_name]); - const meta = useMcpToolMeta(parsed); - const summary = parsed.isMcp - ? getMcpInputSummary(parsed.actionName, request.tool_input) - : ''; - - return ( - onApprove(request.id)} - onCancel={() => onDeny(request.id)} - /> - ); -}; - -// --------------------------------------------------------------------------- -// BatchApprovalWrapper -// --------------------------------------------------------------------------- - -interface BatchApprovalWrapperProps { - requests: ApprovalRequest[]; - onApprove: (requestId: string, updatedInput?: Record) => void; - onDeny: (requestId: string, message?: string) => void; -} - -export const BatchApprovalWrapper: React.FC = ({ - requests, onApprove, onDeny, -}) => { - const questionReqs = useMemo( - () => requests.filter((r) => r.tool_name === 'AskUserQuestion'), - [requests], - ); - const approvalReqs = useMemo( - () => requests.filter((r) => r.tool_name !== 'AskUserQuestion'), - [requests], - ); - - const handleApproveAll = useCallback(() => { - for (const req of approvalReqs) onApprove(req.id); - }, [approvalReqs, onApprove]); - - const handleDenyAll = useCallback(() => { - for (const req of approvalReqs) onDeny(req.id); - }, [approvalReqs, onDeny]); - - return ( -
- {questionReqs.map((req) => ( - - ))} - - {approvalReqs.length > 1 && ( -
-
- - {approvalReqs.length} pending approvals - -
- - -
-
- {approvalReqs.map((req) => ( - - ))} -
- )} - - {approvalReqs.length === 1 && ( - - )} -
- ); -}; - -// --------------------------------------------------------------------------- -// ApprovalRouter (replaces old ApprovalBar default export) -// --------------------------------------------------------------------------- - -interface ApprovalRouterProps { - request: ApprovalRequest; - onApprove: (requestId: string, updatedInput?: Record) => void; - onDeny: (requestId: string, message?: string) => void; -} - -export const ApprovalRouter: React.FC = (props) => { - if (props.request.tool_name === 'AskUserQuestion') { - return ; - } - return ; -}; - -// --------------------------------------------------------------------------- -// Toolkit export (empty — approvals are standalone, not thread tool renderers) -// --------------------------------------------------------------------------- - -export const approvalToolkit: Partial = {}; diff --git a/frontend/src/app/pages/AgentChat/toolkit/approval-utils.tsx b/frontend/src/app/pages/AgentChat/toolkit/approval-utils.tsx deleted file mode 100644 index bbf62ed8..00000000 --- a/frontend/src/app/pages/AgentChat/toolkit/approval-utils.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import React from 'react'; -import type { ReactNode } from 'react'; -// TODO: what is this even supposed to try and import/use ??? -import type { MetadataItem } from '@/components/tool-ui/ApprovalCard/schema'; -import { - Terminal, FileText, FilePen, Search, - MessageCircleQuestion, Wrench, -} from 'lucide-react'; - -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -export interface IntegrationMeta { - label: string; - color: string; - icon: ReactNode; -} - -export interface ParsedTool { - isMcp: boolean; - serverSlug: string; - actionName: string; - displayName: string; -} - -export interface McpToolMeta { - integration: IntegrationMeta | null; - description: string; - serverLabel: string; -} - -// --------------------------------------------------------------------------- -// Integration metadata (ported from approvalUtils.tsx) -// --------------------------------------------------------------------------- - -const GoogleIcon = ( - - - - - - -); - -const RedditIcon = ( - - - - -); - -export const INTEGRATION_META: Record = { - 'Google Workspace': { label: 'Google Workspace', color: '#4285F4', icon: GoogleIcon }, - 'xbird': { label: 'X / Twitter', color: '#1DA1F2', icon: 𝕏 }, - 'Reddit': { label: 'Reddit', color: '#FF4500', icon: RedditIcon }, -}; - -// --------------------------------------------------------------------------- -// Parse / sanitize -// --------------------------------------------------------------------------- - -export function parseMcpToolName(rawName: string): ParsedTool { - const m = rawName.match(/^mcp__([^_]+(?:-[^_]+)*)__(.+)$/); - if (!m) { - return { isMcp: false, serverSlug: '', actionName: rawName, displayName: rawName }; - } - const serverSlug = m[1]; - const actionName = m[2]; - const displayName = actionName - .replace(/_/g, ' ') - .replace(/\b\w/g, (ch) => ch.toUpperCase()); - return { isMcp: true, serverSlug, actionName, displayName }; -} - -export function sanitizeServerName(name: string): string { - return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); -} - -// --------------------------------------------------------------------------- -// MCP input summary -// --------------------------------------------------------------------------- - -export function getMcpInputSummary(actionName: string, toolInput: Record): string { - const lower = actionName.toLowerCase(); - - if (lower.includes('gmail') || lower.includes('email') || lower.includes('mail')) { - const query = toolInput.query || toolInput.search_query || toolInput.q || ''; - const to = toolInput.to || toolInput.recipient || ''; - const subject = toolInput.subject || ''; - if (query) return `Search: "${query}"`; - if (to && subject) return `To ${to} — ${subject}`; - if (to) return `To ${to}`; - if (subject) return `Subject: ${subject}`; - } - - if (lower.includes('calendar') || lower.includes('event') || lower.includes('freebusy')) { - const summary = toolInput.summary || toolInput.title || toolInput.event_name || ''; - const start = toolInput.start || toolInput.start_time || toolInput.date || ''; - if (summary && start) return `${summary} — ${start}`; - if (summary) return summary; - if (start) return `Date: ${start}`; - } - - if (lower.includes('drive') || lower.includes('doc') || lower.includes('sheet') || lower.includes('slide')) { - const name = toolInput.name || toolInput.title || toolInput.filename || toolInput.file_name || ''; - const query = toolInput.query || toolInput.q || ''; - if (name) return name; - if (query) return `Search: "${query}"`; - } - - if (lower.includes('tweet') || lower.includes('post') || lower.includes('send') || lower.includes('reply')) { - const text = toolInput.text || toolInput.content || toolInput.body || toolInput.message || ''; - if (text) return text.length > 80 ? text.slice(0, 77) + '...' : text; - } - - if (lower.includes('search') || lower.includes('find') || lower.includes('query') || lower.includes('list')) { - const query = toolInput.query || toolInput.q || toolInput.search_query || toolInput.keyword || toolInput.term || ''; - if (query) return `"${query}"`; - } - - const stringVals: string[] = []; - for (const [key, val] of Object.entries(toolInput)) { - if (key.startsWith('_')) continue; - if (typeof val === 'string' && val.trim()) { - stringVals.push(val.trim()); - } - if (stringVals.length >= 2) break; - } - if (stringVals.length > 0) { - const joined = stringVals.join(' — '); - return joined.length > 100 ? joined.slice(0, 97) + '...' : joined; - } - - return ''; -} - -// --------------------------------------------------------------------------- -// Icon helpers -// --------------------------------------------------------------------------- - -const TOOL_ICON_MAP: Record = { - Bash: 'terminal', - Read: 'file-text', - Write: 'file-pen', - Edit: 'file-pen', - Grep: 'search', - Glob: 'search', - AskUserQuestion: 'message-circle-question', -}; - -export function getToolIconName(toolName: string): string { - return TOOL_ICON_MAP[toolName] ?? 'wrench'; -} - -/** Backward-compatible JSX icon for external consumers (DynamicIsland, etc.) */ -export function getToolIcon(toolName: string): ReactNode { - const size = 16; - switch (toolName) { - case 'Bash': return ; - case 'Read': return ; - case 'Write': case 'Edit': return ; - case 'Grep': case 'Glob': return ; - case 'AskUserQuestion': return ; - default: return ; - } -} - -// --------------------------------------------------------------------------- -// Metadata / danger helpers -// --------------------------------------------------------------------------- - -export function buildMetadata(toolInput: Record): MetadataItem[] { - return Object.entries(toolInput) - .filter(([, v]) => v != null) - .slice(0, 5) - .map(([key, value]) => ({ - key, - value: typeof value === 'string' - ? value.slice(0, 200) - : JSON.stringify(value).slice(0, 200), - })); -} - -const DANGEROUS_PATTERNS = /\b(rm\s|rmdir|del\s|delete|drop\s|truncate|format)\b/i; - -export function isDangerous(toolName: string, toolInput: Record): boolean { - if (toolName === 'Bash') { - const cmd = toolInput.command || ''; - return DANGEROUS_PATTERNS.test(cmd); - } - return false; -} diff --git a/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/ApprovalRouter.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/ApprovalRouter.tsx new file mode 100644 index 00000000..07dae280 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/ApprovalRouter.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import type { ApprovalRequest } from '@/shared/state/agentsSlice'; +import { ToolQuestion } from './components/ToolQuestion/ToolQuestion'; +import { ToolApproval } from './components/ToolApproval'; + +interface ApprovalRouterProps { + request: ApprovalRequest; + onApprove: (requestId: string, updatedInput?: Record) => void; + onDeny: (requestId: string, message?: string) => void; +} + +export const ApprovalRouter: React.FC = (props) => { + if (props.request.tool_name === 'AskUserQuestion') { + return ; + } + return ; +}; diff --git a/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/BatchApprovalWrapper.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/BatchApprovalWrapper.tsx new file mode 100644 index 00000000..9dab79e1 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/BatchApprovalWrapper.tsx @@ -0,0 +1,73 @@ +import React, { useMemo, useCallback } from 'react'; +import type { ApprovalRequest } from '@/shared/state/agentsSlice'; +import { ToolQuestion } from './components/ToolQuestion/ToolQuestion'; +import { ToolApproval } from './components/ToolApproval'; +import { ApprovalRouter } from './ApprovalRouter'; + +interface BatchApprovalWrapperProps { + requests: ApprovalRequest[]; + onApprove: (requestId: string, updatedInput?: Record) => void; + onDeny: (requestId: string, message?: string) => void; +} + +export const BatchApprovalWrapper: React.FC = ({ + requests, onApprove, onDeny, +}) => { + const questionReqs = useMemo( + () => requests.filter((r) => r.tool_name === 'AskUserQuestion'), + [requests], + ); + const approvalReqs = useMemo( + () => requests.filter((r) => r.tool_name !== 'AskUserQuestion'), + [requests], + ); + + const handleApproveAll = useCallback(() => { + for (const req of approvalReqs) onApprove(req.id); + }, [approvalReqs, onApprove]); + + const handleDenyAll = useCallback(() => { + for (const req of approvalReqs) onDeny(req.id); + }, [approvalReqs, onDeny]); + + return ( +
+ {questionReqs.map((req) => ( + + ))} + + {approvalReqs.length > 1 && ( +
+
+ + {approvalReqs.length} pending approvals + +
+ + +
+
+ {approvalReqs.map((req) => ( + + ))} +
+ )} + + {approvalReqs.length === 1 && ( + + )} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/tool-ui/ApprovalCard/ApprovalCard.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ApprovalCard/ApprovalCard.tsx similarity index 100% rename from frontend/src/components/tool-ui/ApprovalCard/ApprovalCard.tsx rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ApprovalCard/ApprovalCard.tsx diff --git a/frontend/src/components/tool-ui/ApprovalCard/README.md b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ApprovalCard/README.md similarity index 100% rename from frontend/src/components/tool-ui/ApprovalCard/README.md rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ApprovalCard/README.md diff --git a/frontend/src/components/tool-ui/ApprovalCard/_adapter.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ApprovalCard/_adapter.tsx similarity index 100% rename from frontend/src/components/tool-ui/ApprovalCard/_adapter.tsx rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ApprovalCard/_adapter.tsx diff --git a/frontend/src/components/tool-ui/ApprovalCard/schema.ts b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ApprovalCard/schema.ts similarity index 100% rename from frontend/src/components/tool-ui/ApprovalCard/schema.ts rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ApprovalCard/schema.ts diff --git a/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ToolApproval.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ToolApproval.tsx new file mode 100644 index 00000000..b7e43576 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolApproval/ToolApproval.tsx @@ -0,0 +1,90 @@ +import React, { useMemo } from 'react'; +import { ApprovalCard } from './ApprovalCard/ApprovalCard'; +import type { ApprovalRequest } from '@/shared/state/agentsSlice'; +import { useMcpToolMeta } from '../useMcpToolMeta'; +import { + parseMcpToolName, +} from '../../utils'; +import { getMcpInputSummary } from '../getMcpInputSummary'; +// TODO: what is this even supposed to try and import/use ??? +import type { MetadataItem } from '@/components/tool-ui/ApprovalCard/schema'; + + + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + + +const TOOL_ICON_MAP: Record = { + Bash: 'terminal', + Read: 'file-text', + Write: 'file-pen', + Edit: 'file-pen', + Grep: 'search', + Glob: 'search', + AskUserQuestion: 'message-circle-question', +}; + +function getToolIconName(toolName: string): string { + return TOOL_ICON_MAP[toolName] ?? 'wrench'; +} + +function buildMetadata(toolInput: Record): MetadataItem[] { + return Object.entries(toolInput) + .filter(([, v]) => v != null) + .slice(0, 5) + .map(([key, value]) => ({ + key, + value: typeof value === 'string' + ? value.slice(0, 200) + : JSON.stringify(value).slice(0, 200), + })); +} + +const DANGEROUS_PATTERNS = /\b(rm\s|rmdir|del\s|delete|drop\s|truncate|format)\b/i; + +function isDangerous(toolName: string, toolInput: Record): boolean { + if (toolName === 'Bash') { + const cmd = toolInput.command || ''; + return DANGEROUS_PATTERNS.test(cmd); + } + return false; +} + + + +// --------------------------------------------------------------------------- +// Core +// --------------------------------------------------------------------------- + + + +interface ToolApprovalProps { + request: ApprovalRequest; + onApprove: (requestId: string, updatedInput?: Record) => void; + onDeny: (requestId: string, message?: string) => void; +} + +export const ToolApproval: React.FC = ({ request, onApprove, onDeny }) => { + const parsed = useMemo(() => parseMcpToolName(request.tool_name), [request.tool_name]); + const meta = useMcpToolMeta(parsed); + const summary = parsed.isMcp + ? getMcpInputSummary(parsed.actionName, request.tool_input) + : ''; + + return ( + onApprove(request.id)} + onCancel={() => onDeny(request.id)} + /> + ); +}; \ No newline at end of file diff --git a/frontend/src/components/tool-ui/option-list/option-list.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/OptionList.tsx similarity index 98% rename from frontend/src/components/tool-ui/option-list/option-list.tsx rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/OptionList.tsx index 34df143b..c266eaa9 100644 --- a/frontend/src/components/tool-ui/option-list/option-list.tsx +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/OptionList.tsx @@ -18,9 +18,9 @@ import { normalizeSelectionForOptions, parseSelectionToIdSet, } from "./selection"; -import { ActionButtons } from "../shared/action-buttons"; -import { normalizeActionsConfig } from "../shared/actions-config"; -import type { Action } from "../shared/schema"; +import { ActionButtons } from "@/components/tool-ui/shared/action-buttons"; +import { normalizeActionsConfig } from "@/components/tool-ui/shared/actions-config"; +import type { Action } from "@/components/tool-ui/shared/schema"; import { cn, Button, Separator } from "./_adapter"; import { Check } from "lucide-react"; diff --git a/frontend/src/components/tool-ui/option-list/README.md b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/README.md similarity index 100% rename from frontend/src/components/tool-ui/option-list/README.md rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/README.md diff --git a/frontend/src/components/tool-ui/option-list/_adapter.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/_adapter.tsx similarity index 100% rename from frontend/src/components/tool-ui/option-list/_adapter.tsx rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/_adapter.tsx diff --git a/frontend/src/components/tool-ui/option-list/schema.ts b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/schema.ts similarity index 95% rename from frontend/src/components/tool-ui/option-list/schema.ts rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/schema.ts index 840a1fc8..cc65675f 100644 --- a/frontend/src/components/tool-ui/option-list/schema.ts +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/schema.ts @@ -1,14 +1,14 @@ import { z } from "zod"; import type { ReactNode } from "react"; -import type { ActionsProp } from "../shared/actions-config"; -import type { EmbeddedActionsProps } from "../shared/embedded-actions"; +import type { ActionsProp } from "@/components/tool-ui/shared/actions-config"; +import type { EmbeddedActionsProps } from "@/components/tool-ui/shared/embedded-actions"; import { ActionSchema, SerializableActionsConfigSchema, ToolUIIdSchema, ToolUIReceiptSchema, ToolUIRoleSchema, -} from "../shared/schema"; +} from "@/components/tool-ui/shared/schema"; export const OptionListOptionSchema = z.object({ id: z.string().min(1), diff --git a/frontend/src/components/tool-ui/option-list/selection.ts b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/selection.ts similarity index 100% rename from frontend/src/components/tool-ui/option-list/selection.ts rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/OptionList/selection.ts diff --git a/frontend/src/components/tool-ui/question-flow/question-flow.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/QuestionFlow.tsx similarity index 100% rename from frontend/src/components/tool-ui/question-flow/question-flow.tsx rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/QuestionFlow.tsx diff --git a/frontend/src/components/tool-ui/question-flow/README.md b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/README.md similarity index 100% rename from frontend/src/components/tool-ui/question-flow/README.md rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/README.md diff --git a/frontend/src/components/tool-ui/question-flow/_adapter.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/_adapter.tsx similarity index 100% rename from frontend/src/components/tool-ui/question-flow/_adapter.tsx rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/_adapter.tsx diff --git a/frontend/src/components/tool-ui/question-flow/schema.ts b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/schema.ts similarity index 96% rename from frontend/src/components/tool-ui/question-flow/schema.ts rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/schema.ts index 24c00c01..49a32b16 100644 --- a/frontend/src/components/tool-ui/question-flow/schema.ts +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/QuestionFlow/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { ReactNode } from "react"; -import { ToolUIIdSchema, ToolUIRoleSchema } from "../shared/schema"; +import { ToolUIIdSchema, ToolUIRoleSchema } from "@/components/tool-ui/shared/schema"; export const QuestionFlowOptionSchema = z.object({ id: z.string().min(1), diff --git a/frontend/src/app/pages/AgentChat/toolkit/approval-question.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/ToolQuestion.tsx similarity index 96% rename from frontend/src/app/pages/AgentChat/toolkit/approval-question.tsx rename to frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/ToolQuestion.tsx index d842eb28..ed122ccc 100644 --- a/frontend/src/app/pages/AgentChat/toolkit/approval-question.tsx +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/ToolQuestion.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState, useCallback } from 'react'; -import { OptionList } from '@/components/tool-ui/option-list/option-list'; -import type { OptionListSelection } from '@/components/tool-ui/option-list/schema'; -import { QuestionFlow } from '@/components/tool-ui/question-flow/question-flow'; +import { OptionList } from './OptionList/OptionList'; +import type { OptionListSelection } from './OptionList/schema'; +import { QuestionFlow } from './QuestionFlow/QuestionFlow'; import type { ApprovalRequest } from '@/shared/state/agentsSlice'; function optionKey(opt: any): string { diff --git a/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/getMcpInputSummary.ts b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/getMcpInputSummary.ts new file mode 100644 index 00000000..74620e98 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/getMcpInputSummary.ts @@ -0,0 +1,53 @@ +export function getMcpInputSummary(actionName: string, toolInput: Record): string { + const lower = actionName.toLowerCase(); + + if (lower.includes('gmail') || lower.includes('email') || lower.includes('mail')) { + const query = toolInput.query || toolInput.search_query || toolInput.q || ''; + const to = toolInput.to || toolInput.recipient || ''; + const subject = toolInput.subject || ''; + if (query) return `Search: "${query}"`; + if (to && subject) return `To ${to} — ${subject}`; + if (to) return `To ${to}`; + if (subject) return `Subject: ${subject}`; + } + + if (lower.includes('calendar') || lower.includes('event') || lower.includes('freebusy')) { + const summary = toolInput.summary || toolInput.title || toolInput.event_name || ''; + const start = toolInput.start || toolInput.start_time || toolInput.date || ''; + if (summary && start) return `${summary} — ${start}`; + if (summary) return summary; + if (start) return `Date: ${start}`; + } + + if (lower.includes('drive') || lower.includes('doc') || lower.includes('sheet') || lower.includes('slide')) { + const name = toolInput.name || toolInput.title || toolInput.filename || toolInput.file_name || ''; + const query = toolInput.query || toolInput.q || ''; + if (name) return name; + if (query) return `Search: "${query}"`; + } + + if (lower.includes('tweet') || lower.includes('post') || lower.includes('send') || lower.includes('reply')) { + const text = toolInput.text || toolInput.content || toolInput.body || toolInput.message || ''; + if (text) return text.length > 80 ? text.slice(0, 77) + '...' : text; + } + + if (lower.includes('search') || lower.includes('find') || lower.includes('query') || lower.includes('list')) { + const query = toolInput.query || toolInput.q || toolInput.search_query || toolInput.keyword || toolInput.term || ''; + if (query) return `"${query}"`; + } + + const stringVals: string[] = []; + for (const [key, val] of Object.entries(toolInput)) { + if (key.startsWith('_')) continue; + if (typeof val === 'string' && val.trim()) { + stringVals.push(val.trim()); + } + if (stringVals.length >= 2) break; + } + if (stringVals.length > 0) { + const joined = stringVals.join(' — '); + return joined.length > 100 ? joined.slice(0, 97) + '...' : joined; + } + + return ''; + } \ No newline at end of file diff --git a/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/useMcpToolMeta.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/useMcpToolMeta.tsx new file mode 100644 index 00000000..234173ca --- /dev/null +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/components/useMcpToolMeta.tsx @@ -0,0 +1,63 @@ +import { useMemo } from 'react'; +import { useAppSelector } from '@/shared/hooks'; +import type { ToolDefinition } from '@/shared/state/toolsSlice'; +import { + type ParsedTool, type McpToolMeta, type IntegrationMeta, +} from '../utils'; + + +// --------------------------------------------------------------------------- +// Integration metadata (ported from approvalUtils.tsx) +// --------------------------------------------------------------------------- + +const GoogleIcon = ( + + + + + + +); + +const RedditIcon = ( + + + + +); + +const INTEGRATION_META: Record = { + 'Google Workspace': { label: 'Google Workspace', color: '#4285F4', icon: GoogleIcon }, + 'xbird': { label: 'X / Twitter', color: '#1DA1F2', icon: 𝕏 }, + 'Reddit': { label: 'Reddit', color: '#FF4500', icon: RedditIcon }, +}; + + +function sanitizeServerName(name: string): string { + return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); +} + +export function useMcpToolMeta(parsed: ParsedTool): McpToolMeta { + const toolItems = useAppSelector((s) => s.tools.items); + + return useMemo(() => { + if (!parsed.isMcp) { + return { integration: null, description: '', serverLabel: '' }; + } + + const toolDef: ToolDefinition | undefined = Object.values(toolItems).find( + (t) => t.mcp_config && Object.keys(t.mcp_config).length > 0 + && sanitizeServerName(t.name) === parsed.serverSlug, + ); + + if (!toolDef) { + return { integration: null, description: '', serverLabel: parsed.serverSlug }; + } + + const description = toolDef.tool_permissions?._tool_descriptions?.[parsed.actionName] || ''; + const integration = INTEGRATION_META[toolDef.name] || null; + const serverLabel = toolDef.name; + + return { integration, description, serverLabel }; + }, [parsed, toolItems]); +} diff --git a/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/utils.tsx b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/utils.tsx new file mode 100644 index 00000000..c5c2eafc --- /dev/null +++ b/frontend/src/app/pages/AgentChat/toolkit/approvalToolkit/utils.tsx @@ -0,0 +1,43 @@ +import type { ReactNode } from 'react'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface IntegrationMeta { + label: string; + color: string; + icon: ReactNode; +} + +export interface ParsedTool { + isMcp: boolean; + serverSlug: string; + actionName: string; + displayName: string; +} + +export interface McpToolMeta { + integration: IntegrationMeta | null; + description: string; + serverLabel: string; +} + + + +// --------------------------------------------------------------------------- +// Parse / sanitize +// --------------------------------------------------------------------------- + +export function parseMcpToolName(rawName: string): ParsedTool { + const m = rawName.match(/^mcp__([^_]+(?:-[^_]+)*)__(.+)$/); + if (!m) { + return { isMcp: false, serverSlug: '', actionName: rawName, displayName: rawName }; + } + const serverSlug = m[1]; + const actionName = m[2]; + const displayName = actionName + .replace(/_/g, ' ') + .replace(/\b\w/g, (ch) => ch.toUpperCase()); + return { isMcp: true, serverSlug, actionName, displayName }; +} \ No newline at end of file diff --git a/frontend/src/app/pages/AgentChat/toolkit/index.ts b/frontend/src/app/pages/AgentChat/toolkit/index.ts deleted file mode 100644 index d9db5088..00000000 --- a/frontend/src/app/pages/AgentChat/toolkit/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Toolkit } from '@assistant-ui/react'; -import { nativeToolkit } from './native-tools'; -import { approvalToolkit } from './approval-tools'; -import { customToolkit } from './customToolkit/customToolkit'; - -export const toolkit = { - ...nativeToolkit, - ...approvalToolkit, - ...customToolkit, -} as Toolkit; diff --git a/frontend/src/app/pages/AgentChat/toolkit/toolkit.ts b/frontend/src/app/pages/AgentChat/toolkit/toolkit.ts new file mode 100644 index 00000000..aa30c2d5 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/toolkit/toolkit.ts @@ -0,0 +1,11 @@ +import type { Toolkit } from '@assistant-ui/react'; +import { nativeToolkit } from './nativeToolkit/nativeToolkit'; +import { customToolkit } from './customToolkit/customToolkit'; +// import { approvalToolkit } from './approval-tools'; // Not needed bc is empty LEGACY had... +// export const approvalToolkit: Partial = {}; + +export const toolkit = { + ...nativeToolkit, + ...{}, + ...customToolkit, +} as Toolkit; diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/AgentCardCollapsed.tsx b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/AgentCardCollapsed.tsx index 619c22e1..c8cc9743 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/AgentCardCollapsed.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/AgentCardCollapsed.tsx @@ -12,8 +12,8 @@ import TerminalIcon from '@mui/icons-material/Terminal'; import { AgentSession } from '@/shared/state/agentsSlice'; import { HANDLE_APPROVAL } from '@/shared/backend-bridge/apps/agents'; import { useAppDispatch } from '@/shared/hooks'; -import { ToolQuestion } from '@/app/pages/AgentChat/toolkit/approval-question'; -import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approval-utils'; +import { ToolQuestion } from '@/app/pages/AgentChat/toolkit/approvalToolkit/components/ToolQuestion/ToolQuestion'; +import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approvalToolkit/utils'; import GoogleServiceIcon from './components/GoogleServiceIcon'; import { summarizeToolInput } from './components/summarizeToolInput'; import { getToolDisplayName } from './components/getToolDisplayName'; diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/components/getToolDisplayName.ts b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/components/getToolDisplayName.ts index 30a17800..35ce7140 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/components/getToolDisplayName.ts +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/components/getToolDisplayName.ts @@ -1,4 +1,4 @@ -import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approval-utils'; +import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approvalToolkit/utils'; export function getToolDisplayName(toolName: string): string { const mcp = parseMcpToolName(toolName); diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/components/summarizeToolInput.ts b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/components/summarizeToolInput.ts index bd4872a3..8ca3ebfd 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/components/summarizeToolInput.ts +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/AgentCardCollapsed/components/summarizeToolInput.ts @@ -1,4 +1,4 @@ -import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approval-utils'; +import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approvalToolkit/utils'; export function summarizeToolInput(toolName: string, toolInput: Record): string { const mcp = parseMcpToolName(toolName); diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/agentCardUtils.ts b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/agentCardUtils.ts index e4f0739f..54750cee 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/agentCardUtils.ts +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas/cards/AgentCard/components/agentCardUtils.ts @@ -1,5 +1,5 @@ import { AgentSession } from '@/shared/state/agentsSlice'; -import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approval-utils'; +import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approvalToolkit/utils'; export function formatDuration(createdAt: string, closedAt?: string | null, status?: string): string { const start = new Date(createdAt).getTime();