From 63af322ab75287fa6f4a618f93d7db4c3b139d94 Mon Sep 17 00:00:00 2001 From: haikdc Date: Tue, 31 Mar 2026 00:57:33 -0700 Subject: [PATCH] [Haik]: manually removed hella shit that was just not being used at all (the knip linter is clutch) --- .../src/app/pages/AgentChat/DiffViewer.tsx | 2 +- .../app/pages/AgentChat/ToolCallBubble.tsx | 171 ---- .../app/pages/AgentChat/toolCallColors.tsx | 211 ---- .../src/app/pages/AgentChat/toolCallUtils.ts | 130 +-- .../AgentChat/toolkit/approval-question.tsx | 8 +- .../AgentChat/toolkit/approval-tools.tsx | 17 +- .../src/app/pages/AgentChat/toolkit/index.ts | 6 +- .../AgentChat/toolkit/mcp-browser-feed.tsx | 173 ---- .../app/pages/AgentChat/toolkit/mcp-tools.tsx | 177 ---- .../pages/AgentChat/toolkit/native-tools.tsx | 12 +- .../src/app/pages/Analytics/PixelChart.tsx | 77 -- .../pages/Analytics/pixelChartRenderers.ts | 187 ---- .../app/pages/Analytics/pixelChartTypes.ts | 37 - .../src/app/pages/Analytics/usePixelChart.ts | 166 ---- frontend/src/app/pages/Commands/Commands.tsx | 2 - .../pages/Dashboard/AgentCardCollapsed.tsx | 2 +- .../src/app/pages/Dashboard/BrowserCard.tsx | 1 - .../app/pages/Dashboard/CloseAgentDialog.tsx | 60 -- .../app/pages/Dashboard/DashboardCanvas.tsx | 2 +- .../src/app/pages/Dashboard/agentCardUtils.ts | 2 +- .../pages/Dashboard/cardLayoutConstants.ts | 4 +- .../pages/Dashboard/useDashboardSelection.ts | 4 +- .../app/pages/Dashboard/viewCardConstants.ts | 4 +- .../DashboardSelection/DashboardCard.tsx | 2 +- .../app/pages/Settings/CopilotAuthButton.tsx | 112 --- .../app/pages/Settings/SubscriptionCard.tsx | 2 +- .../app/pages/Settings/hooks/useSettings.ts | 24 - .../src/app/pages/Tools/CredentialsDialog.tsx | 2 +- .../src/app/pages/Tools/McpConfigDialog.tsx | 2 +- .../app/pages/Tools/RegistryBrowserDialog.tsx | 2 +- frontend/src/app/pages/Tools/ServiceGroup.tsx | 6 +- frontend/src/app/pages/Tools/ToolSection.tsx | 6 +- frontend/src/app/pages/Views/AutoRunLog.tsx | 2 +- frontend/src/app/pages/Views/ConsolePanel.tsx | 2 +- frontend/src/app/pages/Views/LogEntry.tsx | 2 +- .../components/assistant-ui/attachment.tsx | 32 +- .../src/components/assistant-ui/thread.tsx | 367 ------- .../components/assistant-ui/tool-fallback.tsx | 8 +- .../assistant-ui/tooltip-icon-button.tsx | 2 +- .../tool-ui/approval-card/index.tsx | 7 - .../tool-ui/approval-card/schema.ts | 18 +- .../tool-ui/code-block/code-block.tsx | 6 +- .../components/tool-ui/code-block/index.tsx | 11 - .../components/tool-ui/code-block/schema.ts | 23 +- .../tool-ui/code-diff/code-diff.tsx | 6 +- .../components/tool-ui/code-diff/index.tsx | 7 - .../components/tool-ui/code-diff/schema.ts | 20 - .../components/tool-ui/data-table/README.md | 19 - .../tool-ui/data-table/_adapter.tsx | 23 - .../tool-ui/data-table/data-table.tsx | 936 ------------------ .../tool-ui/data-table/formatters.tsx | 514 ---------- .../components/tool-ui/data-table/types.ts | 262 ----- .../tool-ui/data-table/utilities.ts | 296 ------ .../tool-ui/message-draft/README.md | 19 - .../tool-ui/message-draft/_adapter.tsx | 2 - .../tool-ui/message-draft/index.tsx | 10 - .../tool-ui/message-draft/message-draft.tsx | 511 ---------- .../tool-ui/message-draft/schema.ts | 83 -- .../components/tool-ui/option-list/index.tsx | 7 - .../components/tool-ui/option-list/schema.ts | 38 +- .../tool-ui/progress-tracker/README.md | 19 - .../tool-ui/progress-tracker/_adapter.tsx | 1 - .../tool-ui/progress-tracker/index.tsx | 7 - .../progress-tracker/progress-tracker.tsx | 381 ------- .../tool-ui/progress-tracker/schema.ts | 76 -- .../tool-ui/question-flow/index.tsx | 15 - .../tool-ui/question-flow/question-flow.tsx | 2 +- .../tool-ui/question-flow/schema.ts | 44 +- .../tool-ui/shared/action-buttons.tsx | 2 +- .../src/components/tool-ui/shared/contract.ts | 19 - .../src/components/tool-ui/shared/parse.ts | 51 - .../src/components/tool-ui/shared/schema.ts | 77 +- .../tool-ui/shared/use-action-buttons.tsx | 6 +- .../src/components/tool-ui/terminal/index.tsx | 2 - .../src/components/tool-ui/terminal/schema.ts | 22 +- frontend/src/components/ui/accordion.tsx | 64 -- frontend/src/components/ui/avatar.tsx | 48 - frontend/src/components/ui/badge.tsx | 48 - frontend/src/components/ui/button.tsx | 2 +- frontend/src/components/ui/dropdown-menu.tsx | 257 ----- frontend/src/components/ui/table.tsx | 114 --- frontend/src/shared/browserRegistry.ts | 18 +- frontend/src/shared/config.ts | 3 +- frontend/src/shared/sanitizeSvg.ts | 48 - frontend/src/shared/state/agentsSlice.ts | 4 - .../src/shared/state/dashboardLayoutSlice.ts | 6 +- frontend/src/shared/state/mcpRegistrySlice.ts | 2 +- frontend/src/shared/state/modelsSlice.ts | 2 +- frontend/src/shared/state/outputsSlice.ts | 4 +- frontend/src/shared/state/settingsSlice.ts | 9 - .../src/shared/state/skillRegistrySlice.ts | 3 +- frontend/src/shared/state/toolsSlice.ts | 2 +- frontend/src/shared/state/updateSlice.ts | 2 +- frontend/src/shared/styles/claudeTokens.ts | 3 - frontend/src/shared/useBrowserActivity.ts | 2 +- frontend/src/shared/ws/WebSocketManager.ts | 4 +- frontend/src/shared/ws/wsEventHandlers.ts | 2 +- 97 files changed, 103 insertions(+), 6114 deletions(-) delete mode 100644 frontend/src/app/pages/AgentChat/ToolCallBubble.tsx delete mode 100644 frontend/src/app/pages/AgentChat/toolCallColors.tsx delete mode 100644 frontend/src/app/pages/AgentChat/toolkit/mcp-browser-feed.tsx delete mode 100644 frontend/src/app/pages/AgentChat/toolkit/mcp-tools.tsx delete mode 100644 frontend/src/app/pages/Analytics/PixelChart.tsx delete mode 100644 frontend/src/app/pages/Analytics/pixelChartRenderers.ts delete mode 100644 frontend/src/app/pages/Analytics/pixelChartTypes.ts delete mode 100644 frontend/src/app/pages/Analytics/usePixelChart.ts delete mode 100644 frontend/src/app/pages/Dashboard/CloseAgentDialog.tsx delete mode 100644 frontend/src/app/pages/Settings/CopilotAuthButton.tsx delete mode 100644 frontend/src/components/assistant-ui/thread.tsx delete mode 100644 frontend/src/components/tool-ui/approval-card/index.tsx delete mode 100644 frontend/src/components/tool-ui/code-block/index.tsx delete mode 100644 frontend/src/components/tool-ui/code-diff/index.tsx delete mode 100644 frontend/src/components/tool-ui/data-table/README.md delete mode 100644 frontend/src/components/tool-ui/data-table/_adapter.tsx delete mode 100644 frontend/src/components/tool-ui/data-table/data-table.tsx delete mode 100644 frontend/src/components/tool-ui/data-table/formatters.tsx delete mode 100644 frontend/src/components/tool-ui/data-table/types.ts delete mode 100644 frontend/src/components/tool-ui/data-table/utilities.ts delete mode 100644 frontend/src/components/tool-ui/message-draft/README.md delete mode 100644 frontend/src/components/tool-ui/message-draft/_adapter.tsx delete mode 100644 frontend/src/components/tool-ui/message-draft/index.tsx delete mode 100644 frontend/src/components/tool-ui/message-draft/message-draft.tsx delete mode 100644 frontend/src/components/tool-ui/message-draft/schema.ts delete mode 100644 frontend/src/components/tool-ui/option-list/index.tsx delete mode 100644 frontend/src/components/tool-ui/progress-tracker/README.md delete mode 100644 frontend/src/components/tool-ui/progress-tracker/_adapter.tsx delete mode 100644 frontend/src/components/tool-ui/progress-tracker/index.tsx delete mode 100644 frontend/src/components/tool-ui/progress-tracker/progress-tracker.tsx delete mode 100644 frontend/src/components/tool-ui/progress-tracker/schema.ts delete mode 100644 frontend/src/components/tool-ui/question-flow/index.tsx delete mode 100644 frontend/src/components/tool-ui/shared/contract.ts delete mode 100644 frontend/src/components/tool-ui/shared/parse.ts delete mode 100644 frontend/src/components/tool-ui/terminal/index.tsx delete mode 100644 frontend/src/components/ui/accordion.tsx delete mode 100644 frontend/src/components/ui/badge.tsx delete mode 100644 frontend/src/components/ui/dropdown-menu.tsx delete mode 100644 frontend/src/components/ui/table.tsx delete mode 100644 frontend/src/shared/sanitizeSvg.ts diff --git a/frontend/src/app/pages/AgentChat/DiffViewer.tsx b/frontend/src/app/pages/AgentChat/DiffViewer.tsx index 01df8c0e..91b73fa2 100644 --- a/frontend/src/app/pages/AgentChat/DiffViewer.tsx +++ b/frontend/src/app/pages/AgentChat/DiffViewer.tsx @@ -5,7 +5,7 @@ import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import RefreshIcon from '@mui/icons-material/Refresh'; import DifferenceIcon from '@mui/icons-material/Difference'; -import { CodeDiff } from '@/components/tool-ui/code-diff'; +import { CodeDiff } from '@/components/tool-ui/code-diff/code-diff'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { API_BASE } from '@/shared/config'; diff --git a/frontend/src/app/pages/AgentChat/ToolCallBubble.tsx b/frontend/src/app/pages/AgentChat/ToolCallBubble.tsx deleted file mode 100644 index 8176e908..00000000 --- a/frontend/src/app/pages/AgentChat/ToolCallBubble.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, { useState, useCallback, useMemo } 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 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 ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import BlockIcon from '@mui/icons-material/Block'; -import SearchIcon from '@mui/icons-material/Search'; -import GoogleServiceIcon from '@/app/components/GoogleServiceIcon'; -import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { useTermColors, colorizeInput, colorizeOutput } from './toolCallColors'; -import { ElapsedTimer } from './ElapsedTimer'; -import { BrowserFeedTracker, renderParsedMcpData } from './toolkit/mcp-tools'; -import { InvokeAgentBubble, CreateAgentBubble } from './AgentToolBubble'; -import { - ToolCallBubbleProps, ensureToolCallKeyframes, getToolData, parseMcpToolName, - getMcpShortAction, getInputSummary, formatInputDisplay, parseToolResult, - getResultSummary, getPromptPrefix, formatElapsed, - isBrowserAgentTool, isInvokeAgentTool, isCreateAgentTool, -} from './toolCallUtils'; - -export { parseMcpToolName, getMcpShortAction, getResultSummary } from './toolCallUtils'; -export type { ToolPair, McpToolInfo } from './toolCallUtils'; - -const ToolCallBubble: React.FC = React.memo( - ({ call, result = null, isPending = false, isStreaming = false, mcpCompact = false, sessionId }) => { - ensureToolCallKeyframes(); - const c = useClaudeTokens(); - const tc = useTermColors(); - const [expanded, setExpanded] = useState(false); - const { toolName, input, isDenied } = getToolData(call); - const mcpInfo = useMemo(() => parseMcpToolName(toolName), [toolName]); - const inputSummary = getInputSummary(toolName, input); - const formattedInput = useMemo(() => formatInputDisplay(toolName, input), [toolName, input]); - const showTimer = isPending && !isDenied && !isStreaming; - const isBrowserAgent = isBrowserAgentTool(toolName); - const browserAgentAutoExpand = isBrowserAgent && isPending && !isStreaming; - const showBody = expanded || isStreaming || browserAgentAutoExpand; - const resultContent = result?.content; - const hasStructuredResult = resultContent && typeof resultContent === 'object' && 'text' in resultContent; - const resultRawText: string = hasStructuredResult ? resultContent.text : typeof resultContent === 'string' ? resultContent : resultContent ? JSON.stringify(resultContent, null, 2) : ''; - const resultElapsedMs: number | null = hasStructuredResult ? resultContent.elapsed_ms ?? null : null; - const parsedResult = useMemo(() => (result ? parseToolResult(toolName, resultRawText) : null), [result, toolName, resultRawText]); - const resultSummary = result ? getResultSummary(toolName, resultRawText) : null; - const isError = resultSummary?.startsWith('✗') || (parsedResult?.type === 'bash' && parsedResult.exitCode !== null && parsedResult.exitCode !== 0) || (parsedResult?.type === 'text' && parsedResult.isError); - const toggle = useCallback(() => { if (!isStreaming) setExpanded((v) => !v); }, [isStreaming]); - const accentRgb = c.accent.primary.replace('#', '').match(/.{2}/g)?.map((h) => parseInt(h, 16)).join(', ') || '189, 100, 57'; - const promptPrefix = getPromptPrefix(toolName); - const shortAction = mcpInfo.isMcp ? getMcpShortAction(mcpInfo) : toolName; - const serviceLabel = mcpInfo.isMcp && mcpInfo.service ? mcpInfo.service.charAt(0).toUpperCase() + mcpInfo.service.slice(1) : shortAction; - const ServiceIcon = mcpInfo.isMcp && mcpInfo.service ? : null; - const selectAttrs = { 'data-select-type': 'tool-call' as const, 'data-select-id': call.id, 'data-select-meta': JSON.stringify({ tool: toolName, inputSummary }) }; - - if (isInvokeAgentTool(toolName)) return ; - if (isCreateAgentTool(toolName)) return ; - - if (mcpCompact && mcpInfo.isMcp) { - return ( - - - {ServiceIcon} - {serviceLabel} - {resultSummary && !isError && ( - {resultSummary} - )} - {!resultSummary && !showTimer && } - {showTimer && <>} - {isDenied && ( - - - denied - - )} - {result && !isDenied && ( - - {isError ? : } - {resultElapsedMs != null && {formatElapsed(resultElapsedMs)}} - - )} - - {showBody ? : } - - - - - {isBrowserAgent && sessionId && } - {parsedResult && parsedResult.type === 'mcp' ? ( - renderParsedMcpData(parsedResult.service, parsedResult.action, parsedResult.data, call.id) - ) : parsedResult ? ( -
{parsedResult.type === 'text' ? parsedResult.content : ''}
- ) : null} - {!parsedResult && isPending && !isStreaming && !isBrowserAgent && ( - - )} - -
-
- ); - } - - return ( - - - - {mcpInfo.isMcp && mcpInfo.service - ? - : (() => { const n = toolName.toLowerCase(); if (n.includes('search') || n === 'grep' || n === 'glob') return ; return ; })()} - {mcpInfo.isMcp ? mcpInfo.displayName : toolName} - {mcpInfo.isMcp && {mcpInfo.serverSlug}} - {inputSummary && !isStreaming && {inputSummary}} - {!inputSummary && } - {isStreaming && } - {isDenied && ( - - - denied - - )} - {result && !isDenied && ( - - {isError ? : } - {resultSummary} - {resultElapsedMs != null && {formatElapsed(resultElapsedMs)}} - - )} - {showTimer && } - {!isStreaming && ( - - {showBody ? : } - - )} - - - -
-                {promptPrefix}
-                {isStreaming ? {call.content?.input ?? ''} : colorizeInput(toolName, formattedInput, tc)}
-                {isStreaming && }
-              
- {isBrowserAgent && sessionId && } - {parsedResult && parsedResult.type === 'mcp' ? ( - renderParsedMcpData(parsedResult.service, parsedResult.action, parsedResult.data, call.id) - ) : parsedResult ? ( -
-                  {parsedResult.type === 'bash' ? (
-                    <>
-                      {parsedResult.stdout.trim() && colorizeOutput(toolName, parsedResult.stdout, tc)}
-                      {parsedResult.stderr.trim() && <>{parsedResult.stdout.trim() && '\n'}{parsedResult.stderr}}
-                      {!parsedResult.stdout.trim() && !parsedResult.stderr.trim() && (no output)}
-                    
-                  ) : (
-                    <>{parsedResult.isError ? {parsedResult.content || '(empty)'} : colorizeOutput(toolName, parsedResult.content, tc)}
-                  )}
-                
- ) : null} - {!parsedResult && isPending && !isStreaming && !isBrowserAgent && ( - - )} - -
-
-
- ); - } -); - -export default ToolCallBubble; diff --git a/frontend/src/app/pages/AgentChat/toolCallColors.tsx b/frontend/src/app/pages/AgentChat/toolCallColors.tsx deleted file mode 100644 index 7b5a8c84..00000000 --- a/frontend/src/app/pages/AgentChat/toolCallColors.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import React from 'react'; -import { useThemeMode } from '@/shared/styles/ThemeContext'; -import { parseMcpToolName, isBashTool } from './toolCallUtils'; - -export interface TermColors { - TERM_BG: string; TERM_BORDER: string; PROMPT_COLOR: string; CMD_COLOR: string; - OUTPUT_COLOR: string; PATH_COLOR: string; ADD_COLOR: string; DEL_COLOR: string; - STDERR_COLOR: string; WARN_COLOR: string; NUM_COLOR: string; DIM_COLOR: string; - DIFF_HEADER_COLOR: string; SCROLLBAR_THUMB: string; -} - -const darkTermColors: TermColors = { - TERM_BG: '#131520', TERM_BORDER: '#1e2030', PROMPT_COLOR: '#7ec699', CMD_COLOR: '#e8ecf4', - OUTPUT_COLOR: '#a0aab8', PATH_COLOR: '#82aaff', ADD_COLOR: '#7ec699', DEL_COLOR: '#ff8787', - STDERR_COLOR: '#ff8787', WARN_COLOR: '#ffcb6b', NUM_COLOR: '#f78c6c', DIM_COLOR: '#555b6e', - DIFF_HEADER_COLOR: '#c792ea', SCROLLBAR_THUMB: '#2a2d3e', -}; - -const lightTermColors: TermColors = { - TERM_BG: '#f4f3ee', TERM_BORDER: '#e2e0d8', PROMPT_COLOR: '#2d7a3e', CMD_COLOR: '#2a2a28', - OUTPUT_COLOR: '#555550', PATH_COLOR: '#3060a8', ADD_COLOR: '#2d7a3e', DEL_COLOR: '#c03030', - STDERR_COLOR: '#c03030', WARN_COLOR: '#8a6518', NUM_COLOR: '#c05020', DIM_COLOR: '#9e9c95', - DIFF_HEADER_COLOR: '#7c4daa', SCROLLBAR_THUMB: '#ccc9c0', -}; - -export function useTermColors(): TermColors { - const { mode } = useThemeMode(); - return mode === 'dark' ? darkTermColors : lightTermColors; -} - -export interface CardColors { - TC_BG: string; TC_BORDER: string; TC_HOVER: string; TC_HEADING: string; TC_BODY: string; - TC_MUTED: string; TC_DIM: string; TC_ACCENT: string; TC_SUCCESS: string; TC_WARNING: string; -} - -const darkCardColors: CardColors = { - TC_BG: 'rgba(255,255,255,0.03)', TC_BORDER: 'rgba(255,255,255,0.06)', - TC_HOVER: 'rgba(255,255,255,0.05)', TC_HEADING: '#C2C0B6', TC_BODY: '#9C9A92', - TC_MUTED: '#85837C', TC_DIM: 'rgba(156,154,146,0.5)', - TC_ACCENT: '#c4633a', TC_SUCCESS: '#7AB948', TC_WARNING: '#D1A041', -}; - -const lightCardColors: CardColors = { - TC_BG: 'rgba(0,0,0,0.03)', TC_BORDER: 'rgba(0,0,0,0.08)', - TC_HOVER: 'rgba(0,0,0,0.05)', TC_HEADING: '#3D3D3A', TC_BODY: '#555550', - TC_MUTED: '#73726C', TC_DIM: 'rgba(115,114,108,0.5)', - TC_ACCENT: '#ae5630', TC_SUCCESS: '#265B19', TC_WARNING: '#805C1F', -}; - -export function useCardColors(): CardColors { - const { mode } = useThemeMode(); - return mode === 'dark' ? darkCardColors : lightCardColors; -} - -export function colorizeInput(toolName: string, text: string, tc: TermColors): React.ReactNode { - const n = toolName.toLowerCase(); - const mcp = parseMcpToolName(toolName); - - if (mcp.isMcp) { - const lines = text.split('\n'); - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - const colonIdx = line.indexOf(':'); - if (colonIdx > 0 && colonIdx < 30) { - return ( - - {line.slice(0, colonIdx + 1)} - {line.slice(colonIdx + 1)} - {nl} - - ); - } - return {line}{nl}; - })} - - ); - } - - if (isBashTool(toolName)) return {text}; - - if (n === 'edit' || n === 'strreplace' || n === 'multiedit') { - const lines = text.split('\n'); - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - if (i === 0 && (line.startsWith('/') || line.includes('.'))) - return {line}{nl}; - if (line.startsWith('+ ')) - return {line}{nl}; - if (line.startsWith('- ')) - return {line}{nl}; - return {line}{nl}; - })} - - ); - } - - if (n === 'write') { - const lines = text.split('\n'); - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - if (i === 0 && (line.startsWith('/') || line.includes('.'))) - return {line}{nl}; - return {line}{nl}; - })} - - ); - } - - if (n === 'read' || n === 'glob' || n === 'webfetch') { - if (/^\//.test(text) || text.includes('/')) - return {text}; - } - - if (n === 'grep' || n === 'ripgrep') { - const lines = text.split('\n'); - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - if (line.startsWith('pattern:')) - return ( - - pattern: - {line.slice(9)} - {nl} - - ); - if (line.startsWith('path:')) - return ( - - path: - {line.slice(6)} - {nl} - - ); - return {line}{nl}; - })} - - ); - } - - return {text}; -} - -export function colorizeOutput(toolName: string, text: string, tc: TermColors): React.ReactNode { - if (!text) return (empty); - - const lines = text.split('\n'); - const n = toolName.toLowerCase(); - - return ( - <> - {lines.map((line, i) => { - const nl = i < lines.length - 1 ? '\n' : ''; - const trimmed = line.trimStart(); - - if (/^\/\S+/.test(trimmed)) - return {line}{nl}; - - if (n === 'grep' || n === 'ripgrep') { - const grepMatch = line.match(/^(\S+?:\d+[:-])/); - if (grepMatch) { - return ( - - {grepMatch[1]} - {line.slice(grepMatch[1].length)} - {nl} - - ); - } - const fileHeader = line.match(/^(\S+\.\w+)$/); - if (fileHeader) - return {line}{nl}; - } - - if (line.startsWith('@@') && line.includes('@@')) - return {line}{nl}; - if (line.startsWith('+')) - return {line}{nl}; - if (line.startsWith('-')) - return {line}{nl}; - - if (/\b[Ee]rror\b/.test(line)) - return {line}{nl}; - if (/\b[Ww]arning\b/.test(line)) - return {line}{nl}; - - if (n === 'read') { - const lineNumMatch = line.match(/^(\s*\d+\s*[|:])/); - if (lineNumMatch) { - return ( - - {lineNumMatch[1]} - {line.slice(lineNumMatch[1].length)} - {nl} - - ); - } - } - - return {line}{nl}; - })} - - ); -} diff --git a/frontend/src/app/pages/AgentChat/toolCallUtils.ts b/frontend/src/app/pages/AgentChat/toolCallUtils.ts index cc817146..77e06b46 100644 --- a/frontend/src/app/pages/AgentChat/toolCallUtils.ts +++ b/frontend/src/app/pages/AgentChat/toolCallUtils.ts @@ -1,16 +1,18 @@ import { AgentMessage } from '@/shared/state/agentsSlice'; -export interface ToolPair { type: 'tool_pair'; id: string; call: AgentMessage; result: AgentMessage | null; } -export interface McpToolInfo { isMcp: boolean; serverSlug: string; action: string; service: string; displayName: string; } -export interface ParsedBashResult { type: 'bash'; stdout: string; stderr: string; exitCode: number | null; } -export interface ParsedTextResult { type: 'text'; content: string; isError?: boolean; } -export interface ParsedMcpResult { type: 'mcp'; service: string; action: string; data: Record; rawText: string; } -export type ParsedResult = ParsedBashResult | ParsedTextResult | ParsedMcpResult; export interface ToolCallBubbleProps { call: AgentMessage; result?: AgentMessage | null; isPending?: boolean; isStreaming?: boolean; mcpCompact?: boolean; sessionId?: string; } -export interface InvokeAgentParsed { agentName: string; sessionId: string | null; cost: string | null; response: string; } + +interface McpToolInfo { isMcp: boolean; serverSlug: string; action: string; service: string; displayName: string; } +interface ParsedBashResult { type: 'bash'; stdout: string; stderr: string; exitCode: number | null; } +interface ParsedTextResult { type: 'text'; content: string; isError?: boolean; } +interface ParsedMcpResult { type: 'mcp'; service: string; action: string; data: Record; rawText: string; } +type ParsedResult = ParsedBashResult | ParsedTextResult | ParsedMcpResult; + + +interface InvokeAgentParsed { agentName: string; sessionId: string | null; cost: string | null; response: string; } let toolCallKeyframesInjected = false; export function ensureToolCallKeyframes() { @@ -32,9 +34,9 @@ export function getToolData(call: AgentMessage) { const content = typeof call.content === 'object' ? call.content : {}; return { toolName: content.tool || 'Unknown', input: content.input || {}, isDenied: content.approved === false, toolId: content.id }; } -export function isBashTool(name: string) { return name === 'Bash' || name === 'bash'; } +function isBashTool(name: string) { return name === 'Bash' || name === 'bash'; } -export function parseMcpToolName(rawName: string): McpToolInfo { +function parseMcpToolName(rawName: string): McpToolInfo { const m = rawName.match(/^mcp__([^_]+(?:-[^_]+)*)__(.+)$/); if (!m) return { isMcp: false, serverSlug: '', action: '', service: '', displayName: rawName }; const serverSlug = m[1], action = m[2]; @@ -50,77 +52,6 @@ export function parseMcpToolName(rawName: string): McpToolInfo { return { isMcp: true, serverSlug, action, service, displayName: display }; } -export function getInputSummary(toolName: string, input: any): string { - try { - const mcp = parseMcpToolName(toolName); - if (mcp.isMcp) { - if (!input || typeof input !== 'object') return ''; - const keys = Object.keys(input); - if (keys.length === 0) return ''; - if (keys.length === 1) { const v = input[keys[0]], s = typeof v === 'string' ? v : JSON.stringify(v); return s.length > 60 ? s.slice(0, 60) + '…' : s; } - return keys.slice(0, 3).map((k) => { const v = input[k], s = typeof v === 'string' ? v : JSON.stringify(v); return `${k}: ${s.length > 30 ? s.slice(0, 30) + '…' : s}`; }).join(' '); - } - const n = toolName.toLowerCase(); - if (isBashTool(toolName)) { const cmd = input.command || ''; return `$ ${cmd.slice(0, 80)}${cmd.length > 80 ? '…' : ''}`; } - if (n === 'read' || n === 'write') return input.file_path || input.path || ''; - if (n === 'edit' || n === 'multiedit' || n === 'strreplace') return input.file_path || input.path || ''; - if (n === 'glob') return input.pattern || input.glob || input.glob_pattern || ''; - if (n === 'grep' || n === 'ripgrep') { - const pat = input.pattern || input.regex || '', path = input.path || input.directory || ''; - return path ? `/${pat}/ in ${path}` : `/${pat}/`; - } - if (n === 'websearch') return input.query || input.search_term || ''; - if (n === 'webfetch') return input.url || ''; - if (n === 'todoread' || n === 'todowrite') return 'todos'; - if (n === 'ls') return input.path || '.'; - return ''; - } catch { return ''; } -} -export function formatInputDisplay(toolName: string, input: any): string { - try { - const mcp = parseMcpToolName(toolName); - if (mcp.isMcp) { - if (!input || typeof input !== 'object') return String(input ?? ''); - return Object.entries(input).map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v, null, 2)}`).join('\n'); - } - const n = toolName.toLowerCase(); - if (isBashTool(toolName)) return input.command || ''; - if (n === 'read') { - const p = input.file_path || input.path || '', parts = [p]; - if (input.offset) parts.push(`offset: ${input.offset}`); - if (input.limit) parts.push(`limit: ${input.limit}`); - return parts.join(' '); - } - if (n === 'write') { - const p = input.file_path || input.path || '', c = input.content || ''; - return `${p}\n\n${c.length > 300 ? c.slice(0, 300) + '\n…' : c}`; - } - if (n === 'edit' || n === 'strreplace') { - const p = input.file_path || input.path || '', old = input.old_string || input.old_text || '', nw = input.new_string || input.new_text || ''; - const lines = [p, '']; - if (old) { const o = old.length > 200 ? old.slice(0, 200) + '…' : old; lines.push(`- ${o.split('\n').join('\n- ')}`); } - if (nw) { const n2 = nw.length > 200 ? nw.slice(0, 200) + '…' : nw; lines.push(`+ ${n2.split('\n').join('\n+ ')}`); } - return lines.join('\n'); - } - if (n === 'multiedit') { - const edits = input.edits || [], lines = [input.file_path || input.path || '']; - for (const e of edits.slice(0, 3)) lines.push(` - ${(e.old_string || e.old_text || '').split('\n')[0].slice(0, 60)}…`); - if (edits.length > 3) lines.push(` … +${edits.length - 3} more edits`); - return lines.join('\n'); - } - if (n === 'glob') return input.pattern || input.glob || input.glob_pattern || ''; - if (n === 'grep' || n === 'ripgrep') { - const pat = input.pattern || input.regex || '', path = input.path || input.directory || ''; - const parts = [`pattern: ${pat}`]; - if (path) parts.push(`path: ${path}`); - if (input.include) parts.push(`include: ${input.include}`); - return parts.join('\n'); - } - if (n === 'websearch') return input.query || input.search_term || ''; - if (n === 'webfetch') return input.url || ''; - } catch {} - return typeof input === 'string' ? input : JSON.stringify(input, null, 2); -} export function parseToolResult(toolName: string, rawText: string): ParsedResult { if (isBashTool(toolName)) { try { @@ -155,13 +86,14 @@ export function parseToolResult(toolName: string, rawText: string): ParsedResult } catch {} return { type: 'text', content: rawText }; } -export function getMcpShortAction(mcpInfo: McpToolInfo): string { - let short = mcpInfo.action; - if (mcpInfo.service && short.toLowerCase().startsWith(mcpInfo.service.toLowerCase() + '_')) short = short.slice(mcpInfo.service.length + 1); - return short.replace(/_/g, ' ').replace(/\b\w/g, (ch) => ch.toUpperCase()); + +interface GmailHeaderSource { + payload?: { headers?: Array<{ name: string; value: string }> }; + headers?: Record; + [key: string]: unknown; } -export function getGmailHeader(msg: any, name: string): string { - if (msg.payload?.headers && Array.isArray(msg.payload.headers)) { const h = msg.payload.headers.find((hdr: any) => (hdr.name || '').toLowerCase() === name.toLowerCase()); if (h) return h.value || ''; } +function getGmailHeader(msg: GmailHeaderSource, name: string): string { + if (msg.payload?.headers && Array.isArray(msg.payload.headers)) { const h = msg.payload.headers.find((hdr) => (hdr.name || '').toLowerCase() === name.toLowerCase()); if (h) return h.value || ''; } if (msg.headers && typeof msg.headers === 'object' && !Array.isArray(msg.headers)) return msg.headers[name] || msg.headers[name.toLowerCase()] || ''; return ''; } @@ -194,31 +126,7 @@ export function getResultSummary(toolName: string, rawText: string): string { } catch {} return `${lc} line${lc !== 1 ? 's' : ''}`; } -export function getPromptPrefix(toolName: string): string { - if (isBashTool(toolName)) return '$ '; - const mcp = parseMcpToolName(toolName); - return mcp.isMcp ? `❯ ${mcp.displayName} ` : `❯ ${toolName} `; -} -export function formatTimestamp(ts: string | number | undefined): string { - if (!ts) return ''; - try { - const d = typeof ts === 'number' ? new Date(ts) : new Date(ts); - if (isNaN(d.getTime())) return String(ts); - return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }); - } catch { return String(ts); } -} -export function stripHtml(html: string): string { - const tmp = document.createElement('div'); tmp.innerHTML = html; return tmp.textContent || tmp.innerText || ''; -} -export function isBrowserAgentTool(name: string): boolean { - if (name === 'CreateBrowserAgent' || name === 'BrowserAgent' || name === 'BrowserAgents') return true; - const m = parseMcpToolName(name); return m.isMcp && m.serverSlug === 'openswarm-browser-agent'; -} -export function isInvokeAgentTool(name: string): boolean { - if (name === 'InvokeAgent') return true; - const m = parseMcpToolName(name); return m.isMcp && m.serverSlug === 'openswarm-invoke-agent'; -} -export function isCreateAgentTool(name: string): boolean { return name === 'Agent'; } + export function parseInvokedSessionId(rawText: string): string | null { return rawText.match(/\(forked session:\s*([a-f0-9]+)\)/)?.[1] || null; } export function parseCreateAgentResult(rawText: string): string { if (!rawText) return ''; diff --git a/frontend/src/app/pages/AgentChat/toolkit/approval-question.tsx b/frontend/src/app/pages/AgentChat/toolkit/approval-question.tsx index 21a6ed45..d842eb28 100644 --- a/frontend/src/app/pages/AgentChat/toolkit/approval-question.tsx +++ b/frontend/src/app/pages/AgentChat/toolkit/approval-question.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState, useCallback } from 'react'; -import { OptionList } from '@/components/tool-ui/option-list'; -import type { OptionListSelection } from '@/components/tool-ui/option-list'; -import { QuestionFlow } from '@/components/tool-ui/question-flow'; +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 type { ApprovalRequest } from '@/shared/state/agentsSlice'; function optionKey(opt: any): string { @@ -71,7 +71,7 @@ const FreeTextQuestion: React.FC<{ // ToolQuestion // --------------------------------------------------------------------------- -export interface ToolQuestionProps { +interface ToolQuestionProps { request: ApprovalRequest; onApprove: (requestId: string, updatedInput?: Record) => void; onDeny: (requestId: string, message?: string) => void; diff --git a/frontend/src/app/pages/AgentChat/toolkit/approval-tools.tsx b/frontend/src/app/pages/AgentChat/toolkit/approval-tools.tsx index 069d2fe3..37593260 100644 --- a/frontend/src/app/pages/AgentChat/toolkit/approval-tools.tsx +++ b/frontend/src/app/pages/AgentChat/toolkit/approval-tools.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useCallback } from 'react'; import type { Toolkit } from '@assistant-ui/react'; -import { ApprovalCard } from '@/components/tool-ui/approval-card'; +import { ApprovalCard } from '@/components/tool-ui/approval-card/approval-card'; import type { ApprovalRequest } from '@/shared/state/agentsSlice'; import { useAppSelector } from '@/shared/hooks'; import type { ToolDefinition } from '@/shared/state/toolsSlice'; @@ -14,15 +14,10 @@ import { ToolQuestion } from './approval-question'; // Re-exports so external consumers can import everything from this file export { - parseMcpToolName, sanitizeServerName, getMcpInputSummary, - getToolIcon, getToolIconName, buildMetadata, isDangerous, - INTEGRATION_META, -} from './approval-utils'; -export type { - IntegrationMeta, ParsedTool, McpToolMeta, + parseMcpToolName, + getToolIcon, } from './approval-utils'; export { ToolQuestion } from './approval-question'; -export type { ToolQuestionProps } from './approval-question'; // --------------------------------------------------------------------------- // useMcpToolMeta (React hook — lives here alongside other component code) @@ -57,13 +52,13 @@ export function useMcpToolMeta(parsed: ParsedTool): McpToolMeta { // ToolApproval // --------------------------------------------------------------------------- -export interface ToolApprovalProps { +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 ToolApproval: React.FC = ({ request, onApprove, onDeny }) => { const parsed = useMemo(() => parseMcpToolName(request.tool_name), [request.tool_name]); const meta = useMcpToolMeta(parsed); const summary = parsed.isMcp @@ -90,7 +85,7 @@ export const ToolApproval: React.FC = ({ request, onApprove, // BatchApprovalWrapper // --------------------------------------------------------------------------- -export interface BatchApprovalWrapperProps { +interface BatchApprovalWrapperProps { requests: ApprovalRequest[]; onApprove: (requestId: string, updatedInput?: Record) => void; onDeny: (requestId: string, message?: string) => void; diff --git a/frontend/src/app/pages/AgentChat/toolkit/index.ts b/frontend/src/app/pages/AgentChat/toolkit/index.ts index 006d45a2..79cc0cdb 100644 --- a/frontend/src/app/pages/AgentChat/toolkit/index.ts +++ b/frontend/src/app/pages/AgentChat/toolkit/index.ts @@ -1,12 +1,10 @@ import type { Toolkit } from '@assistant-ui/react'; import { nativeToolkit } from './native-tools'; import { approvalToolkit } from './approval-tools'; -import { mcpToolkit } from './mcp-tools'; import { customToolkit } from './custom-tools'; -export const toolkit: Toolkit = { +export const toolkit = { ...nativeToolkit, ...approvalToolkit, - ...mcpToolkit, ...customToolkit, -}; +} as Toolkit; diff --git a/frontend/src/app/pages/AgentChat/toolkit/mcp-browser-feed.tsx b/frontend/src/app/pages/AgentChat/toolkit/mcp-browser-feed.tsx deleted file mode 100644 index 6c9d12a8..00000000 --- a/frontend/src/app/pages/AgentChat/toolkit/mcp-browser-feed.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import React, { useMemo, useEffect, useRef } from 'react'; -import { createSelector } from '@reduxjs/toolkit'; -import { ProgressTracker } from '@/components/tool-ui/progress-tracker'; -import type { ProgressStep } from '@/components/tool-ui/progress-tracker'; -import { useAppSelector, useAppDispatch } from '@/shared/hooks'; -import type { AgentSession, AgentMessage } from '@/shared/state/agentsSlice'; -import { fetchBrowserAgentChildren } from '@/shared/state/agentsSlice'; -import type { RootState } from '@/shared/state/store'; - -// --------------------------------------------------------------------------- -// Selectors -// --------------------------------------------------------------------------- - -const selectBrowserSessions = createSelector( - [ - (state: RootState) => state.agents.sessions, - (_: RootState, parentSessionId: string) => parentSessionId, - (_: RootState, __: string, browserId?: string) => browserId, - ], - (sessions, parentSessionId, browserId) => - Object.values(sessions).filter( - (s): s is AgentSession => - s.mode === 'browser-agent' && - s.parent_session_id === parentSessionId && - (!browserId || s.browser_id === browserId), - ), -); - -// --------------------------------------------------------------------------- -// Message → step conversion -// --------------------------------------------------------------------------- - -function formatBrowserAction(content: any): { label: string; description: string } { - const tool = content?.tool || content?.name || '?'; - const input = content?.input || {}; - - switch (tool) { - case 'BrowserNavigate': - return { label: 'Navigate', description: input.url || '...' }; - case 'BrowserClick': - return { label: 'Click', description: input.selector || '...' }; - case 'BrowserType': { - const txt = (input.text || '').slice(0, 40); - const ellipsis = (input.text || '').length > 40 ? '…' : ''; - return { label: 'Type', description: `"${txt}${ellipsis}" → ${input.selector || '...'}` }; - } - case 'BrowserScreenshot': - return { label: 'Screenshot', description: 'Capture page' }; - case 'BrowserGetText': - return { label: 'Read text', description: 'Get page content' }; - case 'BrowserGetElements': - return { label: 'Inspect', description: input.selector ? `Elements (${input.selector})` : 'Elements' }; - case 'BrowserEvaluate': - return { label: 'Execute JS', description: 'Run script' }; - default: - return { label: tool, description: JSON.stringify(input).slice(0, 60) }; - } -} - -function messagesToSteps(messages: AgentMessage[]): ProgressStep[] { - const steps: ProgressStep[] = []; - const pendingCalls = new Map(); - - for (const msg of messages) { - if (msg.role === 'tool_call') { - const content = - typeof msg.content === 'string' - ? (() => { try { return JSON.parse(msg.content); } catch { return {}; } })() - : msg.content; - - const { label, description } = formatBrowserAction(content); - const stepId = content?.id || `step-${steps.length}`; - - steps.push({ id: stepId, label, description, status: 'in-progress' }); - if (content?.id) pendingCalls.set(content.id, steps.length - 1); - } else if (msg.role === 'tool_result') { - const content = - typeof msg.content === 'string' - ? (() => { try { return JSON.parse(msg.content); } catch { return { text: msg.content }; } })() - : msg.content; - - const callId = content?.tool_call_id || content?.id; - if (callId && pendingCalls.has(callId)) { - const idx = pendingCalls.get(callId)!; - steps[idx] = { - ...steps[idx], - status: content?.is_error || content?.error ? 'failed' : 'completed', - }; - pendingCalls.delete(callId); - } else { - const lastIdx = [...pendingCalls.values()].pop(); - if (lastIdx !== undefined) { - steps[lastIdx] = { - ...steps[lastIdx], - status: content?.is_error || content?.error ? 'failed' : 'completed', - }; - for (const [k, v] of pendingCalls) { - if (v === lastIdx) { pendingCalls.delete(k); break; } - } - } - } - } - } - - return steps; -} - -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - -interface BrowserFeedTrackerProps { - parentSessionId: string; - browserId?: string; -} - -export const BrowserFeedTracker: React.FC = ({ - parentSessionId, - browserId, -}) => { - const dispatch = useAppDispatch(); - const fetchedRef = useRef(null); - - const browserSessions = useAppSelector((state) => - selectBrowserSessions(state, parentSessionId, browserId), - ); - - useEffect(() => { - if (browserSessions.length === 0 && fetchedRef.current !== parentSessionId) { - fetchedRef.current = parentSessionId; - dispatch(fetchBrowserAgentChildren(parentSessionId)) - .unwrap() - .catch(() => { fetchedRef.current = null; }); - } - }, [browserSessions.length, parentSessionId, dispatch]); - - const allSteps = useMemo(() => { - const raw: ProgressStep[] = []; - for (const session of browserSessions) { - raw.push(...messagesToSteps(session.messages)); - } - if (raw.length === 0) return raw; - - const seen = new Set(); - return raw.map((step, i) => { - let id = step.id; - if (seen.has(id)) id = `${id}-${i}`; - seen.add(id); - return { ...step, id }; - }); - }, [browserSessions]); - - if (browserSessions.length === 0 || allSteps.length === 0) return null; - - const allDone = allSteps.every((s) => s.status === 'completed' || s.status === 'failed'); - const hasFailed = allSteps.some((s) => s.status === 'failed'); - - return ( - - ); -}; diff --git a/frontend/src/app/pages/AgentChat/toolkit/mcp-tools.tsx b/frontend/src/app/pages/AgentChat/toolkit/mcp-tools.tsx deleted file mode 100644 index ff1dc77c..00000000 --- a/frontend/src/app/pages/AgentChat/toolkit/mcp-tools.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import type { ReactNode } from 'react'; -import type { Toolkit } from '@assistant-ui/react'; -import { MessageDraft } from '@/components/tool-ui/message-draft'; -import { DataTable } from '@/components/tool-ui/data-table/data-table'; -import { - getGmailHeader, formatTimestamp, stripHtml, -} from '../toolCallUtils'; - -export { BrowserFeedTracker } from './mcp-browser-feed'; - -// -- Helpers ---------------------------------------------------------------- - -function extractEmailFields(msg: any) { - const subject = msg.subject || getGmailHeader(msg, 'Subject') || '(no subject)'; - const from = msg.from || msg.sender || getGmailHeader(msg, 'From') || ''; - const to = msg.to || msg.recipient || getGmailHeader(msg, 'To') || ''; - const rawDate = msg.date || msg.internalDate || msg.receivedAt || getGmailHeader(msg, 'Date') || ''; - const date = formatTimestamp(rawDate); - const snippet = msg.snippet || ''; - const body = msg.body || msg.text || msg.textBody || ''; - const htmlBody = msg.htmlBody || msg.html || ''; - const bodyPreview = body || (htmlBody ? stripHtml(htmlBody) : ''); - return { subject, from, to, date, snippet, bodyPreview }; -} - -// -- Gmail → MessageDraft / DataTable --------------------------------------- - -function renderGmailSingle(data: Record, toolCallId: string): ReactNode { - const email = extractEmailFields(data); - const raw = email.to || ''; - const toArray = typeof raw === 'string' - ? raw.split(',').map((s: string) => s.trim()).filter(Boolean) - : Array.isArray(raw) ? raw : []; - - return ( - 0 ? toArray : ['(unknown)']} - body={email.bodyPreview || email.snippet || '(empty)'} - outcome="sent" - /> - ); -} - -function renderGmailList(messages: any[], toolCallId: string): ReactNode { - return ( - { - const f = extractEmailFields(msg); - return { - id: String(i), - from: f.from, - subject: f.subject, - date: f.date, - snippet: (f.snippet || f.bodyPreview || '').slice(0, 120), - }; - })} - /> - ); -} - -function renderGmailResult(data: Record, action: string, toolCallId: string): ReactNode { - const isSearch = action.includes('search') || action.includes('list'); - const messages: any[] = data.messages || (isSearch && data.results ? data.results : []); - if (messages.length > 0) return renderGmailList(messages, toolCallId); - return renderGmailSingle(data, toolCallId); -} - -// -- Calendar → DataTable --------------------------------------------------- - -function renderCalendarResult(data: Record, toolCallId: string): ReactNode { - const items: any[] = data.items || (Array.isArray(data) ? data : []); - const single = !items.length && (data.summary || data.start) ? data : null; - const rows = single ? [single] : items; - if (rows.length === 0) return null; - - return ( - ({ - id: String(i), - summary: item.summary || '(no title)', - start: item.start?.dateTime || item.start?.date || item.start || '', - end: item.end?.dateTime || item.end?.date || item.end || '', - location: item.location || '', - }))} - /> - ); -} - -// -- Drive → DataTable ------------------------------------------------------ - -function renderDriveResult(data: Record, toolCallId: string): ReactNode { - const files: any[] = data.files || (Array.isArray(data) ? data : []); - const single = !files.length && data.name ? data : null; - const rows = single ? [single] : files; - if (rows.length === 0) return null; - - return ( - ({ - id: String(i), - name: f.name || f.id || '', - mimeType: f.mimeType?.split('/').pop() || '', - }))} - /> - ); -} - -// -- Generic MCP fallback → DataTable (key / value) ------------------------- - -function renderGenericMcp(data: Record, toolCallId: string): ReactNode { - const entries = Object.entries(data).filter(([, v]) => v != null); - if (entries.length === 0) return null; - - return ( - ({ - id: String(i), - field: key, - value: typeof val === 'object' - ? JSON.stringify(val, null, 2).slice(0, 500) - : String(val).slice(0, 500), - }))} - /> - ); -} - -// -- MCP result dispatch ---------------------------------------------------- - -/** Render already-parsed MCP data (used by ToolCallBubble). */ -export function renderParsedMcpData( - service: string, action: string, data: Record, toolCallId: string, -): ReactNode | null { - if (data.error || data.is_error) return null; - switch (service) { - case 'gmail': return renderGmailResult(data, action, toolCallId); - case 'calendar': return renderCalendarResult(data, toolCallId); - case 'drive': case 'sheets': return renderDriveResult(data, toolCallId); - default: - return Object.keys(data).length > 0 ? renderGenericMcp(data, toolCallId) : null; - } -} - -// -- Exported toolkit (empty — MCP tool names are dynamic; Agent 7 wires) --- - -export const mcpToolkit: Partial = {}; diff --git a/frontend/src/app/pages/AgentChat/toolkit/native-tools.tsx b/frontend/src/app/pages/AgentChat/toolkit/native-tools.tsx index 896ad910..caabe336 100644 --- a/frontend/src/app/pages/AgentChat/toolkit/native-tools.tsx +++ b/frontend/src/app/pages/AgentChat/toolkit/native-tools.tsx @@ -1,8 +1,8 @@ import type { ReactNode } from 'react'; import type { Toolkit } from '@assistant-ui/react'; -import { Terminal } from '@/components/tool-ui/terminal'; -import { CodeBlock } from '@/components/tool-ui/code-block'; -import { CodeDiff } from '@/components/tool-ui/code-diff'; +import { Terminal } from '@/components/tool-ui/terminal/terminal'; +import { CodeBlock } from '@/components/tool-ui/code-block/code-block'; +import { CodeDiff } from '@/components/tool-ui/code-diff/code-diff'; // -- Helpers ---------------------------------------------------------------- @@ -107,7 +107,7 @@ const readRenderer = be(({ args: raw, result, status, toolCallId }) => { // -- Write → CodeBlock (shows written content from args) -------------------- -const writeRenderer = be(({ args: raw, result, status, toolCallId }) => { +const writeRenderer = be(({ args: raw, toolCallId }) => { const a = toArgs(raw); const filePath = s(a, 'file_path', 'path'); return ( @@ -121,7 +121,7 @@ const writeRenderer = be(({ args: raw, result, status, toolCallId }) => { // -- Edit / StrReplace → CodeDiff ------------------------------------------- -const editRenderer = be(({ args: raw, result, status, toolCallId }) => { +const editRenderer = be(({ args: raw, result, toolCallId }) => { const a = toArgs(raw); const filePath = s(a, 'file_path', 'path'); const oldCode = s(a, 'old_string', 'old_text'); @@ -144,7 +144,7 @@ const editRenderer = be(({ args: raw, result, status, toolCallId }) => { // -- MultiEdit → CodeDiff (first edit) -------------------------------------- -const multiEditRenderer = be(({ args: raw, result, status, toolCallId }) => { +const multiEditRenderer = be(({ args: raw, result, toolCallId }) => { const a = toArgs(raw); const filePath = s(a, 'file_path', 'path'); const edits = Array.isArray(a.edits) ? a.edits : []; diff --git a/frontend/src/app/pages/Analytics/PixelChart.tsx b/frontend/src/app/pages/Analytics/PixelChart.tsx deleted file mode 100644 index 845db171..00000000 --- a/frontend/src/app/pages/Analytics/PixelChart.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import { PixelChartProps } from './pixelChartTypes'; -import { usePixelChart } from './usePixelChart'; - -const PixelChart: React.FC = ({ - data, - palette = 'salmon', - height = 140, - pixelSize = 6, - formatValue, - glow = true, - showXLabels = true, - showYScale = true, - mode = 'bar', -}) => { - const { - canvasRef, containerRef, tooltipRef, - xLabels, Y_LABEL_WIDTH, - handleMouseMove, handleMouseLeave, c, - } = usePixelChart({ data, palette, height, pixelSize, formatValue, glow, showYScale, mode }); - - return ( - - - {showXLabels && data.length > 0 && ( - - {xLabels.map((xl) => ( - - {xl.label} - - ))} - - )} - - - ); -}; - -export default PixelChart; diff --git a/frontend/src/app/pages/Analytics/pixelChartRenderers.ts b/frontend/src/app/pages/Analytics/pixelChartRenderers.ts deleted file mode 100644 index 80db4684..00000000 --- a/frontend/src/app/pages/Analytics/pixelChartRenderers.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { ChartDrawParams } from './pixelChartTypes'; - -export function drawYAxis( - ctx: CanvasRenderingContext2D, - yTicks: number[], - effectiveMax: number, - h: number, - px: number, - yLabelWidth: number, - totalW: number, - formatValue: ((v: number) => string) | undefined, - borderSubtle: string, - textGhost: string, -) { - ctx.font = '10px monospace'; - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - - for (const tick of yTicks) { - const yNorm = effectiveMax > 0 ? tick / effectiveMax : 0; - const yPx = h - yNorm * (h - px); - - ctx.strokeStyle = borderSubtle; - ctx.lineWidth = 0.5; - ctx.setLineDash([2, 4]); - ctx.beginPath(); - ctx.moveTo(yLabelWidth, yPx); - ctx.lineTo(totalW, yPx); - ctx.stroke(); - ctx.setLineDash([]); - - const label = formatValue ? formatValue(tick) : (tick % 1 === 0 ? String(tick) : tick.toFixed(1)); - ctx.fillStyle = textGhost; - ctx.fillText(label, yLabelWidth - 8, yPx); - } -} - -export function drawGridDots( - ctx: CanvasRenderingContext2D, - gridRows: number, - gridCols: number, - px: number, - yLabelWidth: number, - borderSubtle: string, -) { - ctx.fillStyle = borderSubtle; - for (let gy = 0; gy < gridRows; gy += 5) { - for (let gx = 0; gx < gridCols; gx += 5) { - ctx.fillRect(yLabelWidth + gx * px, gy * px, 1, 1); - } - } -} - -export function drawAreaChart(p: ChartDrawParams) { - const { ctx, data, h, px, chartW, effectiveMax, yLabelWidth, progress, hoverIdx, colors, glow } = p; - const usableH = h - px * 2; - const points: { x: number; y: number }[] = []; - - for (let i = 0; i < data.length; i++) { - const val = data[i].value; - const norm = effectiveMax > 0 ? val / effectiveMax : 0; - const x = yLabelWidth + (i / Math.max(data.length - 1, 1)) * chartW; - const y = h - px - norm * usableH * progress; - points.push({ x, y }); - } - - if (points.length === 0) return; - - const gradient = ctx.createLinearGradient(0, 0, 0, h); - gradient.addColorStop(0, colors[colors.length - 1] + '60'); - gradient.addColorStop(0.5, colors[Math.floor(colors.length / 2)] + '30'); - gradient.addColorStop(1, colors[0] + '08'); - - ctx.beginPath(); - ctx.moveTo(points[0].x, h); - for (let i = 0; i < points.length; i++) { - if (i === 0) { - ctx.lineTo(points[i].x, points[i].y); - } else { - const prev = points[i - 1]; - const curr = points[i]; - const cpx = (prev.x + curr.x) / 2; - ctx.bezierCurveTo(cpx, prev.y, cpx, curr.y, curr.x, curr.y); - } - } - ctx.lineTo(points[points.length - 1].x, h); - ctx.closePath(); - ctx.fillStyle = gradient; - ctx.fill(); - - ctx.beginPath(); - for (let i = 0; i < points.length; i++) { - if (i === 0) { - ctx.moveTo(points[i].x, points[i].y); - } else { - const prev = points[i - 1]; - const curr = points[i]; - const cpx = (prev.x + curr.x) / 2; - ctx.bezierCurveTo(cpx, prev.y, cpx, curr.y, curr.x, curr.y); - } - } - ctx.strokeStyle = colors[colors.length - 1]; - ctx.lineWidth = 2; - ctx.stroke(); - - if (glow) { - ctx.shadowColor = colors[colors.length - 1]; - ctx.shadowBlur = 8; - ctx.stroke(); - ctx.shadowBlur = 0; - } - - for (let i = 0; i < points.length; i++) { - if (data[i].value > 0) { - const isHov = i === hoverIdx; - ctx.beginPath(); - ctx.arc(points[i].x, points[i].y, isHov ? 4 : 2.5, 0, Math.PI * 2); - ctx.fillStyle = isHov ? colors[colors.length - 1] : colors[Math.floor(colors.length / 2)]; - ctx.fill(); - if (isHov) { - ctx.strokeStyle = colors[colors.length - 1]; - ctx.lineWidth = 1.5; - ctx.stroke(); - } - } - } - - for (let i = 0; i < points.length - 1; i++) { - const p1 = points[i]; - const p2 = points[i + 1]; - const steps = Math.ceil((p2.x - p1.x) / px); - for (let s = 0; s < steps; s++) { - const t = s / steps; - const x = p1.x + t * (p2.x - p1.x); - const lineY = p1.y + t * (p2.y - p1.y); - for (let py = lineY + px * 2; py < h - px; py += px * 2) { - if (Math.random() > 0.65) { - const depth = (py - lineY) / (h - lineY); - const ci = Math.max(0, Math.floor((1 - depth) * (colors.length - 1))); - ctx.globalAlpha = 0.15 + (1 - depth) * 0.2; - ctx.fillStyle = colors[ci]; - ctx.fillRect(Math.floor(x / px) * px, Math.floor(py / px) * px, px - 1, px - 1); - } - } - } - } - ctx.globalAlpha = 1; -} - -export function drawBarChart(p: ChartDrawParams) { - const { ctx, data, gridRows, gridCols, effectiveMax, yLabelWidth, px, progress, hoverIdx, colors, glow } = p; - const barSlots = data.length; - const totalBarPx = Math.max(1, Math.floor(gridCols / barSlots)); - const barW = Math.max(1, totalBarPx - 1); - - for (let i = 0; i < data.length; i++) { - const val = data[i].value; - const normalised = effectiveMax > 0 ? val / effectiveMax : 0; - const usableRows = gridRows - 2; - const targetH = Math.max(normalised > 0 ? 1 : 0, Math.round(normalised * usableRows)); - const barH = Math.round(targetH * progress); - const barX = i * totalBarPx; - const isHovered = i === hoverIdx; - - for (let row = 0; row < barH; row++) { - const y = gridRows - 1 - row; - const colorIdx = Math.min(colors.length - 1, Math.floor((row / Math.max(barH - 1, 1)) * (colors.length - 1))); - const baseColor = isHovered ? colors[Math.min(colorIdx + 1, colors.length - 1)] : colors[colorIdx]; - - for (let col = 0; col < barW; col++) { - ctx.fillStyle = baseColor; - ctx.fillRect(yLabelWidth + (barX + col) * px, y * px, px - 1, px - 1); - } - } - - if (glow && barH > 0) { - const topY = (gridRows - 1 - barH + 1) * px; - ctx.shadowColor = colors[colors.length - 1]; - ctx.shadowBlur = 6; - ctx.fillStyle = colors[colors.length - 1]; - for (let col = 0; col < barW; col++) { - ctx.fillRect(yLabelWidth + (barX + col) * px, topY, px - 1, px - 1); - } - ctx.shadowBlur = 0; - } - } -} diff --git a/frontend/src/app/pages/Analytics/pixelChartTypes.ts b/frontend/src/app/pages/Analytics/pixelChartTypes.ts deleted file mode 100644 index 8e90a4a0..00000000 --- a/frontend/src/app/pages/Analytics/pixelChartTypes.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const PALETTES = { - salmon: ['#C46B57', '#D4795F', '#E8927A', '#F0A088', '#F5B49E'], - blue: ['#445588', '#5577AA', '#6688BB', '#7799CC', '#88AADD'], - coral: ['#993344', '#AA3D4E', '#BB4455', '#CC5566', '#DD6677'], - green: ['#447755', '#558866', '#669977', '#77AA88', '#88BB99'], - purple: ['#665588', '#7766AA', '#8877BB', '#9988CC', '#AA99DD'], -} as const; - -export type PaletteKey = keyof typeof PALETTES; - -export interface PixelChartProps { - data: { label: string; value: number }[]; - palette?: PaletteKey; - height?: number; - pixelSize?: number; - formatValue?: (v: number) => string; - glow?: boolean; - showXLabels?: boolean; - showYScale?: boolean; - mode?: 'bar' | 'area'; -} - -export interface ChartDrawParams { - ctx: CanvasRenderingContext2D; - data: { label: string; value: number }[]; - h: number; - px: number; - gridCols: number; - gridRows: number; - chartW: number; - effectiveMax: number; - yLabelWidth: number; - progress: number; - hoverIdx: number; - colors: readonly string[]; - glow: boolean; -} diff --git a/frontend/src/app/pages/Analytics/usePixelChart.ts b/frontend/src/app/pages/Analytics/usePixelChart.ts deleted file mode 100644 index 97dbcc69..00000000 --- a/frontend/src/app/pages/Analytics/usePixelChart.ts +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useRef, useEffect, useCallback } from 'react'; -import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { PALETTES, PixelChartProps } from './pixelChartTypes'; -import { drawYAxis, drawGridDots, drawAreaChart, drawBarChart } from './pixelChartRenderers'; - -export function computeYTicks(maxVal: number): number[] { - if (maxVal <= 0) return [0]; - const rawStep = maxVal / 3; - const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep))); - const normalised = rawStep / magnitude; - let niceStep: number; - if (normalised <= 1) niceStep = magnitude; - else if (normalised <= 2) niceStep = 2 * magnitude; - else if (normalised <= 5) niceStep = 5 * magnitude; - else niceStep = 10 * magnitude; - const ticks: number[] = []; - for (let v = 0; v <= maxVal * 1.1; v += niceStep) { - ticks.push(v); - } - if (ticks.length < 2) ticks.push(niceStep); - return ticks; -} - -export function computeXLabels(data: { label: string; value: number }[]): { idx: number; label: string }[] { - if (data.length <= 1) return data.map((d, i) => ({ idx: i, label: d.label })); - if (data.length <= 5) return data.map((d, i) => ({ idx: i, label: d.label })); - const result: { idx: number; label: string }[] = []; - result.push({ idx: 0, label: data[0].label }); - const step = Math.floor(data.length / 4); - for (let i = 1; i <= 3; i++) { - const idx = Math.min(i * step, data.length - 2); - if (idx > 0 && idx < data.length - 1) { - result.push({ idx, label: data[idx].label }); - } - } - result.push({ idx: data.length - 1, label: data[data.length - 1].label }); - return result; -} - -type UsePixelChartProps = Required> & - Pick; - -export function usePixelChart({ - data, - palette = 'salmon', - height = 140, - pixelSize = 6, - formatValue, - glow = true, - showYScale = true, - mode = 'bar', -}: UsePixelChartProps) { - const canvasRef = useRef(null); - const containerRef = useRef(null); - const animRef = useRef(0); - const progressRef = useRef(0); - const hoverIdxRef = useRef(-1); - const tooltipRef = useRef(null); - const c = useClaudeTokens(); - const colors = PALETTES[palette]; - - const maxVal = Math.max(...data.map((d) => d.value), 0.001); - const yTicks = computeYTicks(maxVal); - const xLabels = computeXLabels(data); - const Y_LABEL_WIDTH = showYScale ? 80 : 0; - - const draw = useCallback(() => { - const canvas = canvasRef.current; - const container = containerRef.current; - if (!canvas || !container || data.length === 0) return; - - const dpr = window.devicePixelRatio || 1; - const totalW = container.clientWidth; - const chartW = totalW - Y_LABEL_WIDTH; - const h = height; - canvas.width = totalW * dpr; - canvas.height = h * dpr; - canvas.style.width = `${totalW}px`; - canvas.style.height = `${h}px`; - - const ctx = canvas.getContext('2d'); - if (!ctx) return; - ctx.scale(dpr, dpr); - - const px = pixelSize; - const gridCols = Math.floor(chartW / px); - const gridRows = Math.floor(h / px); - const effectiveMax = yTicks[yTicks.length - 1] || maxVal; - - ctx.clearRect(0, 0, totalW, h); - - if (showYScale) { - drawYAxis(ctx, yTicks, effectiveMax, h, px, Y_LABEL_WIDTH, totalW, formatValue, c.border.subtle, c.text.ghost); - } - drawGridDots(ctx, gridRows, gridCols, px, Y_LABEL_WIDTH, c.border.subtle); - - const progress = Math.min(progressRef.current, 1); - const hoverIdx = hoverIdxRef.current; - const params = { ctx, data, h, px, gridCols, gridRows, chartW, effectiveMax, yLabelWidth: Y_LABEL_WIDTH, progress, hoverIdx, colors, glow }; - - if (mode === 'area') { - drawAreaChart(params); - } else { - drawBarChart(params); - } - }, [data, height, pixelSize, c, colors, glow, maxVal, yTicks, showYScale, Y_LABEL_WIDTH, formatValue, mode]); - - useEffect(() => { - progressRef.current = 0; - let start: number | null = null; - const animate = (ts: number) => { - if (!start) start = ts; - progressRef.current = Math.min(1, (ts - start) / 600); - draw(); - if (progressRef.current < 1) animRef.current = requestAnimationFrame(animate); - }; - animRef.current = requestAnimationFrame(animate); - return () => cancelAnimationFrame(animRef.current); - }, [data, draw]); - - useEffect(() => { - const handleResize = () => draw(); - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [draw]); - - const handleMouseMove = useCallback( - (e: React.MouseEvent) => { - const canvas = canvasRef.current; - const tooltip = tooltipRef.current; - if (!canvas || !tooltip || data.length === 0) return; - - const rect = canvas.getBoundingClientRect(); - const mx = e.clientX - rect.left - Y_LABEL_WIDTH; - if (mx < 0) { hoverIdxRef.current = -1; tooltip.style.opacity = '0'; draw(); return; } - - const chartW = rect.width - Y_LABEL_WIDTH; - const gridCols = Math.floor(chartW / pixelSize); - const totalBarPx = Math.max(1, Math.floor(gridCols / data.length)); - const idx = Math.floor(mx / (totalBarPx * pixelSize)); - - if (idx >= 0 && idx < data.length) { - hoverIdxRef.current = idx; - const d = data[idx]; - const valStr = formatValue ? formatValue(d.value) : d.value.toFixed(2); - tooltip.textContent = `${d.label}: ${valStr}`; - tooltip.style.opacity = '1'; - tooltip.style.left = `${e.clientX - rect.left}px`; - tooltip.style.top = `${e.clientY - rect.top - 28}px`; - } else { - hoverIdxRef.current = -1; - tooltip.style.opacity = '0'; - } - draw(); - }, - [data, pixelSize, draw, formatValue, Y_LABEL_WIDTH], - ); - - const handleMouseLeave = useCallback(() => { - hoverIdxRef.current = -1; - if (tooltipRef.current) tooltipRef.current.style.opacity = '0'; - draw(); - }, [draw]); - - return { canvasRef, containerRef, tooltipRef, xLabels, Y_LABEL_WIDTH, handleMouseMove, handleMouseLeave, c }; -} diff --git a/frontend/src/app/pages/Commands/Commands.tsx b/frontend/src/app/pages/Commands/Commands.tsx index d6acca1e..5cc04f8f 100644 --- a/frontend/src/app/pages/Commands/Commands.tsx +++ b/frontend/src/app/pages/Commands/Commands.tsx @@ -20,5 +20,3 @@ export const CommandsContent: React.FC = () => { ); }; - -export default CommandsContent; diff --git a/frontend/src/app/pages/Dashboard/AgentCardCollapsed.tsx b/frontend/src/app/pages/Dashboard/AgentCardCollapsed.tsx index d1a6a74c..588ba7ad 100644 --- a/frontend/src/app/pages/Dashboard/AgentCardCollapsed.tsx +++ b/frontend/src/app/pages/Dashboard/AgentCardCollapsed.tsx @@ -12,7 +12,7 @@ import TerminalIcon from '@mui/icons-material/Terminal'; import { AgentSession, handleApproval } from '@/shared/state/agentsSlice'; import { useAppDispatch } from '@/shared/hooks'; import { ToolQuestion as QuestionForm } from '@/app/pages/AgentChat/toolkit/approval-tools'; -import { parseMcpToolName } from '@/app/pages/AgentChat/ToolCallBubble'; +import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approval-utils'; import GoogleServiceIcon from '@/app/components/GoogleServiceIcon'; import { summarizeToolInput, getToolDisplayName } from './agentCardUtils'; diff --git a/frontend/src/app/pages/Dashboard/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/BrowserCard.tsx index 8f9d514e..68a0093f 100644 --- a/frontend/src/app/pages/Dashboard/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/BrowserCard.tsx @@ -15,7 +15,6 @@ import BrowserTabBar from './BrowserTabBar'; import BrowserNavBar from './BrowserNavBar'; import BrowserActionOverlay from './BrowserActionOverlay'; -export type { TabLocalState, WebviewElement }; const MIN_W = 400, MIN_H = 300; interface Props { diff --git a/frontend/src/app/pages/Dashboard/CloseAgentDialog.tsx b/frontend/src/app/pages/Dashboard/CloseAgentDialog.tsx deleted file mode 100644 index 541dbf83..00000000 --- a/frontend/src/app/pages/Dashboard/CloseAgentDialog.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import Dialog from '@mui/material/Dialog'; -import DialogTitle from '@mui/material/DialogTitle'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogActions from '@mui/material/DialogActions'; -import Button from '@mui/material/Button'; -import { useClaudeTokens } from '@/shared/styles/ThemeContext'; - -interface Props { - open: boolean; - onCancel: () => void; - onConfirm: () => void; -} - -const CloseAgentDialog: React.FC = ({ open, onCancel, onConfirm }) => { - const c = useClaudeTokens(); - return ( - - - Agent still running - - - - This agent is still running. Closing it will pause the agent. - You can resume it later from the chat history. - - - - - - - - ); -}; - -export default CloseAgentDialog; diff --git a/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx b/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx index 714012be..e3421a1e 100644 --- a/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardCanvas.tsx @@ -16,7 +16,7 @@ import type { CanvasActions } from './useCanvasControls'; const TETHER_FADE_MS = 2500; -export interface DashboardCanvasProps { +interface DashboardCanvasProps { panX: number; panY: number; zoom: number; isPanning: boolean; spaceHeld: boolean; cmdHeld: boolean; viewportRef: React.RefObject; contentRef: React.RefObject; diff --git a/frontend/src/app/pages/Dashboard/agentCardUtils.ts b/frontend/src/app/pages/Dashboard/agentCardUtils.ts index 07755fc6..e4f0739f 100644 --- a/frontend/src/app/pages/Dashboard/agentCardUtils.ts +++ b/frontend/src/app/pages/Dashboard/agentCardUtils.ts @@ -1,5 +1,5 @@ import { AgentSession } from '@/shared/state/agentsSlice'; -import { parseMcpToolName } from '@/app/pages/AgentChat/ToolCallBubble'; +import { parseMcpToolName } from '@/app/pages/AgentChat/toolkit/approval-utils'; export function formatDuration(createdAt: string, closedAt?: string | null, status?: string): string { const start = new Date(createdAt).getTime(); diff --git a/frontend/src/app/pages/Dashboard/cardLayoutConstants.ts b/frontend/src/app/pages/Dashboard/cardLayoutConstants.ts index fc1f7202..df4d444c 100644 --- a/frontend/src/app/pages/Dashboard/cardLayoutConstants.ts +++ b/frontend/src/app/pages/Dashboard/cardLayoutConstants.ts @@ -1,7 +1,7 @@ export type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'; -export const EDGE_THICKNESS = 6; -export const CORNER_SIZE = 14; +const EDGE_THICKNESS = 6; +const CORNER_SIZE = 14; export const DRAG_THRESHOLD = 3; export const CURSOR_MAP: Record = { diff --git a/frontend/src/app/pages/Dashboard/useDashboardSelection.ts b/frontend/src/app/pages/Dashboard/useDashboardSelection.ts index aa84f6e2..fd81a583 100644 --- a/frontend/src/app/pages/Dashboard/useDashboardSelection.ts +++ b/frontend/src/app/pages/Dashboard/useDashboardSelection.ts @@ -3,12 +3,12 @@ import type { CardPosition, ViewCardPosition, BrowserCardPosition } from '@/shar export type CardType = 'agent' | 'view' | 'browser'; -export interface SelectedCard { +interface SelectedCard { id: string; type: CardType; } -export interface MarqueeRect { +interface MarqueeRect { x: number; y: number; width: number; diff --git a/frontend/src/app/pages/Dashboard/viewCardConstants.ts b/frontend/src/app/pages/Dashboard/viewCardConstants.ts index c53d28f2..ddc4c353 100644 --- a/frontend/src/app/pages/Dashboard/viewCardConstants.ts +++ b/frontend/src/app/pages/Dashboard/viewCardConstants.ts @@ -2,8 +2,8 @@ import type { Output } from '@/shared/state/outputsSlice'; export type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'; -export const EDGE_THICKNESS = 6; -export const CORNER_SIZE = 14; +const EDGE_THICKNESS = 6; +const CORNER_SIZE = 14; export const MIN_W = 320; export const MIN_H = 200; diff --git a/frontend/src/app/pages/DashboardSelection/DashboardCard.tsx b/frontend/src/app/pages/DashboardSelection/DashboardCard.tsx index cb579cd9..cab402ed 100644 --- a/frontend/src/app/pages/DashboardSelection/DashboardCard.tsx +++ b/frontend/src/app/pages/DashboardSelection/DashboardCard.tsx @@ -9,7 +9,7 @@ import MoreVertIcon from '@mui/icons-material/MoreVert'; import { Dashboard } from '@/shared/state/dashboardsSlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -export function formatRelativeTime(dateStr: string | null): string { +function formatRelativeTime(dateStr: string | null): string { if (!dateStr) return ''; const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000); if (seconds < 60) return 'just now'; diff --git a/frontend/src/app/pages/Settings/CopilotAuthButton.tsx b/frontend/src/app/pages/Settings/CopilotAuthButton.tsx deleted file mode 100644 index f5c6ca92..00000000 --- a/frontend/src/app/pages/Settings/CopilotAuthButton.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import Button from '@mui/material/Button'; -import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { API_BASE } from '@/shared/config'; - -const CopilotAuthButton: React.FC = () => { - const c = useClaudeTokens(); - const [status, setStatus] = useState<'idle' | 'waiting' | 'connected' | 'error'>('idle'); - const [userCode, setUserCode] = useState(''); - const [username, setUsername] = useState(''); - const [error, setError] = useState(''); - - useEffect(() => { - fetch(`${API_BASE}/agents/copilot/models`) - .then(r => r.json()) - .then(d => { - if (d.models && d.models.length > 0) setStatus('connected'); - }) - .catch(() => {}); - }, []); - - const startAuth = async () => { - setStatus('waiting'); - setError(''); - try { - const resp = await fetch(`${API_BASE}/agents/copilot/start-auth`, { method: 'POST' }); - const data = await resp.json(); - setUserCode(data.user_code); - window.open(data.verification_uri, '_blank'); - - const deviceCode = data.device_code; - const poll = setInterval(async () => { - try { - const r = await fetch(`${API_BASE}/agents/copilot/poll-auth`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ device_code: deviceCode }), - }); - const d = await r.json(); - if (d.status === 'connected') { - clearInterval(poll); - setStatus('connected'); - setUsername(d.username || ''); - } - } catch {} - }, 5000); - - setTimeout(() => { clearInterval(poll); if (status === 'waiting') { setStatus('error'); setError('Auth timed out'); } }, 300000); - } catch (e: any) { - setStatus('error'); - setError(e.message || 'Failed to start auth'); - } - }; - - const disconnect = async () => { - await fetch(`${API_BASE}/agents/copilot/disconnect`, { method: 'POST' }); - setStatus('idle'); - setUsername(''); - }; - - if (status === 'connected') { - return ( - - - - Connected{username ? ` as @${username}` : ''} - - - Disconnect - - - ); - } - - if (status === 'waiting') { - return ( - - - Enter code {userCode} at github.com/login/device - - Waiting for authorization... - - ); - } - - return ( - - - {error && {error}} - - ); -}; - -export default CopilotAuthButton; diff --git a/frontend/src/app/pages/Settings/SubscriptionCard.tsx b/frontend/src/app/pages/Settings/SubscriptionCard.tsx index eb5e9d8d..e4f11e10 100644 --- a/frontend/src/app/pages/Settings/SubscriptionCard.tsx +++ b/frontend/src/app/pages/Settings/SubscriptionCard.tsx @@ -9,7 +9,7 @@ export const SUBSCRIPTION_PROVIDERS = [ { id: 'github', name: 'GitHub Copilot', desc: 'Claude + GPT models via your Copilot subscription', color: '#8B949E', preview: true }, ]; -export type SubscriptionProvider = typeof SUBSCRIPTION_PROVIDERS[0]; +type SubscriptionProvider = typeof SUBSCRIPTION_PROVIDERS[0]; interface SubscriptionCardProps { provider: SubscriptionProvider; diff --git a/frontend/src/app/pages/Settings/hooks/useSettings.ts b/frontend/src/app/pages/Settings/hooks/useSettings.ts index c7081818..0ec20a7d 100644 --- a/frontend/src/app/pages/Settings/hooks/useSettings.ts +++ b/frontend/src/app/pages/Settings/hooks/useSettings.ts @@ -6,30 +6,6 @@ import { setChecking, setUpdateError } from '@/shared/state/updateSlice'; import { fetchModes } from '@/shared/state/modesSlice'; import { useClaudeTokens, useThemeMode } from '@/shared/styles/ThemeContext'; -export const API_KEY_STEPS = [ - { - title: 'Open the Anthropic Console', - detail: 'Visit console.anthropic.com — create a free account if you don\'t have one yet.', - link: 'https://console.anthropic.com', - }, - { - title: 'Navigate to API Keys', - detail: 'In the dashboard, click "Settings" in the left sidebar, then select "API Keys".', - }, - { - title: 'Create a new key', - detail: 'Click the "Create Key" button. Name it anything you like (e.g. "OpenSwarm").', - }, - { - title: 'Copy your key', - detail: 'Click the copy icon next to your new key. It will start with sk-ant-api03-…', - }, - { - title: 'Paste it above & save', - detail: 'Paste the key into the field above, then hit Save. You\'re all set!', - }, -]; - export function useSettings() { const c = useClaudeTokens(); const dispatch = useAppDispatch(); diff --git a/frontend/src/app/pages/Tools/CredentialsDialog.tsx b/frontend/src/app/pages/Tools/CredentialsDialog.tsx index 302c5236..00e2b3ec 100644 --- a/frontend/src/app/pages/Tools/CredentialsDialog.tsx +++ b/frontend/src/app/pages/Tools/CredentialsDialog.tsx @@ -12,7 +12,7 @@ import LinkIcon from '@mui/icons-material/Link'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { Integration } from './integrations'; -export interface CredentialsDialogProps { +interface CredentialsDialogProps { open: boolean; onClose: () => void; integration: Integration | null; diff --git a/frontend/src/app/pages/Tools/McpConfigDialog.tsx b/frontend/src/app/pages/Tools/McpConfigDialog.tsx index 41b2805d..818a9558 100644 --- a/frontend/src/app/pages/Tools/McpConfigDialog.tsx +++ b/frontend/src/app/pages/Tools/McpConfigDialog.tsx @@ -17,7 +17,7 @@ import { McpServer } from '@/shared/state/mcpRegistrySlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { cleanServerName } from './toolUtils'; -export interface McpConfigDialogProps { +interface McpConfigDialogProps { open: boolean; onClose: () => void; mcpConfigServer: McpServer | null; diff --git a/frontend/src/app/pages/Tools/RegistryBrowserDialog.tsx b/frontend/src/app/pages/Tools/RegistryBrowserDialog.tsx index dddf184b..0aab1a63 100644 --- a/frontend/src/app/pages/Tools/RegistryBrowserDialog.tsx +++ b/frontend/src/app/pages/Tools/RegistryBrowserDialog.tsx @@ -32,7 +32,7 @@ import { ToolDefinition } from '@/shared/state/toolsSlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { cleanServerName } from './toolUtils'; -export interface RegistryBrowserDialogProps { +interface RegistryBrowserDialogProps { open: boolean; onClose: () => void; regQuery: string; diff --git a/frontend/src/app/pages/Tools/ServiceGroup.tsx b/frontend/src/app/pages/Tools/ServiceGroup.tsx index 649ae280..2ab5078e 100644 --- a/frontend/src/app/pages/Tools/ServiceGroup.tsx +++ b/frontend/src/app/pages/Tools/ServiceGroup.tsx @@ -9,7 +9,7 @@ import VisibilityIcon from '@mui/icons-material/Visibility'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { PermToggle } from './PermToggle'; -export function toDisplayName(name: string, serviceName?: string): string { +function toDisplayName(name: string, serviceName?: string): string { let display = name.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); if (serviceName) { const svcLower = serviceName.toLowerCase(); @@ -28,7 +28,7 @@ export function firstSentence(desc: string): string { return match ? match[1].trim() : desc.substring(0, 100); } -export function getGroupPolicy(names: string[], perms: Record): string { +function getGroupPolicy(names: string[], perms: Record): string { if (names.length === 0) return 'ask'; const policies = names.map((n) => perms[n] || 'ask'); if (policies.every((p) => p === 'always_allow')) return 'always_allow'; @@ -82,7 +82,7 @@ const ToolRow = ({ name, serviceName, desc, schema, schemaKey, perms, devMode, e ); }; -const ToolGroup = ({ label, icon, iconColor, names, serviceName, descriptions, schemas, perms, devMode, expandedSchema, setExpandedSchema, toolId, onPermissionChange, onGroupPermissionChange, c }: any) => { +const ToolGroup = ({ label, icon, names, serviceName, descriptions, schemas, perms, devMode, expandedSchema, setExpandedSchema, toolId, onPermissionChange, onGroupPermissionChange, c }: any) => { if (!names || names.length === 0) return null; const gp = getGroupPolicy(names, perms); return ( diff --git a/frontend/src/app/pages/Tools/ToolSection.tsx b/frontend/src/app/pages/Tools/ToolSection.tsx index 8a3c86d7..1ca27c50 100644 --- a/frontend/src/app/pages/Tools/ToolSection.tsx +++ b/frontend/src/app/pages/Tools/ToolSection.tsx @@ -20,7 +20,7 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { CATEGORY_ORDER } from './integrations'; import { PermToggle } from './PermToggle'; -export interface ToolSectionProps { +interface ToolSectionProps { label: string; icon: React.ReactElement; count: number; @@ -42,7 +42,7 @@ export interface ToolSectionProps { export const ToolSection: React.FC = ({ label, icon, count, open, onToggle, grouped, collapsedCategories, toggleCategory, - expandedBuiltin, toggleBuiltinExpand, deferred, + deferred, builtinPermissions, onPermissionChange, onCategoryPermissionChange, enabled, onEnabledChange, }) => { @@ -66,8 +66,6 @@ export const ToolSection: React.FC = ({ return 'mixed'; }; - const allSectionTools = CATEGORY_ORDER.filter((cat) => grouped[cat]).flatMap((cat) => grouped[cat]); - const categoryCount = CATEGORY_ORDER.filter((cat) => grouped[cat]).length; const sectionDescription = deferred ? 'On-demand actions loaded via ToolSearch for planning, scheduling, and extended operations' : 'Built-in Claude Agent SDK actions for file operations, shell commands, and search'; diff --git a/frontend/src/app/pages/Views/AutoRunLog.tsx b/frontend/src/app/pages/Views/AutoRunLog.tsx index 82bb9ebe..931bf08f 100644 --- a/frontend/src/app/pages/Views/AutoRunLog.tsx +++ b/frontend/src/app/pages/Views/AutoRunLog.tsx @@ -8,7 +8,7 @@ import { AgentMessage } from '@/shared/state/agentsSlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { LogEntry } from './LogEntry'; -export interface AutoRunLogProps { +interface AutoRunLogProps { messages: AgentMessage[]; status: string | null; logEndRef: React.RefObject; diff --git a/frontend/src/app/pages/Views/ConsolePanel.tsx b/frontend/src/app/pages/Views/ConsolePanel.tsx index cf8a56d1..5b7cee6b 100644 --- a/frontend/src/app/pages/Views/ConsolePanel.tsx +++ b/frontend/src/app/pages/Views/ConsolePanel.tsx @@ -16,7 +16,7 @@ export interface ConsoleEntry { running?: boolean; } -export interface ConsolePanelProps { +interface ConsolePanelProps { entry: ConsoleEntry | null; c: ReturnType; } diff --git a/frontend/src/app/pages/Views/LogEntry.tsx b/frontend/src/app/pages/Views/LogEntry.tsx index e879850a..1337907d 100644 --- a/frontend/src/app/pages/Views/LogEntry.tsx +++ b/frontend/src/app/pages/Views/LogEntry.tsx @@ -7,7 +7,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { AgentMessage } from '@/shared/state/agentsSlice'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -export interface LogEntryProps { +interface LogEntryProps { msg: AgentMessage; c: ReturnType; } diff --git a/frontend/src/components/assistant-ui/attachment.tsx b/frontend/src/components/assistant-ui/attachment.tsx index 55fefb3b..3f189805 100644 --- a/frontend/src/components/assistant-ui/attachment.tsx +++ b/frontend/src/components/assistant-ui/attachment.tsx @@ -1,10 +1,9 @@ "use client"; import { PropsWithChildren, useEffect, useState, type FC } from "react"; -import { XIcon, PlusIcon, FileText } from "lucide-react"; +import { XIcon, FileText } from "lucide-react"; import { AttachmentPrimitive, - ComposerPrimitive, MessagePrimitive, useAuiState, useAui, @@ -193,31 +192,4 @@ export const UserMessageAttachments: FC = () => { ); -}; - -export const ComposerAttachments: FC = () => { - return ( -
- - {() => } - -
- ); -}; - -export const ComposerAddAttachment: FC = () => { - return ( - - - - - - ); -}; +}; \ No newline at end of file diff --git a/frontend/src/components/assistant-ui/thread.tsx b/frontend/src/components/assistant-ui/thread.tsx deleted file mode 100644 index dae56cb3..00000000 --- a/frontend/src/components/assistant-ui/thread.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { - ComposerAddAttachment, - ComposerAttachments, - UserMessageAttachments, -} from "@/components/assistant-ui/attachment"; -import { MarkdownText } from "@/components/assistant-ui/markdown-text"; -import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; -import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import { - ActionBarMorePrimitive, - ActionBarPrimitive, - AuiIf, - BranchPickerPrimitive, - ComposerPrimitive, - ErrorPrimitive, - MessagePrimitive, - SuggestionPrimitive, - ThreadPrimitive, - useAuiState, -} from "@assistant-ui/react"; -import { - ArrowDownIcon, - ArrowUpIcon, - CheckIcon, - ChevronLeftIcon, - ChevronRightIcon, - CopyIcon, - DownloadIcon, - MoreHorizontalIcon, - PencilIcon, - RefreshCwIcon, - SquareIcon, -} from "lucide-react"; -import type { FC } from "react"; - -export const Thread: FC = () => { - return ( - - - s.thread.isEmpty}> - - - - - {() => } - - - - - - - - - ); -}; - -const ThreadMessage: FC = () => { - const role = useAuiState((s) => s.message.role); - const isEditing = useAuiState((s) => s.message.composer.isEditing); - if (isEditing) return ; - if (role === "user") return ; - return ; -}; - -const ThreadScrollToBottom: FC = () => { - return ( - - - - - - ); -}; - -const ThreadWelcome: FC = () => { - return ( -
-
-
-

- Hello there! -

-

- How can I help you today? -

-
-
- -
- ); -}; - -const ThreadSuggestions: FC = () => { - return ( -
- - {() => } - -
- ); -}; - -const ThreadSuggestionItem: FC = () => { - return ( -
- - - -
- ); -}; - -const Composer: FC = () => { - return ( - - -
- - - -
-
-
- ); -}; - -const ComposerAction: FC = () => { - return ( -
- - !s.thread.isRunning}> - - - - - - - s.thread.isRunning}> - - - - -
- ); -}; - -const MessageError: FC = () => { - return ( - - - - - - ); -}; - -const AssistantMessage: FC = () => { - return ( - -
- - {({ part }) => { - if (part.type === "text") return ; - if (part.type === "tool-call") - return part.toolUI ?? ; - return null; - }} - - -
- -
- - -
-
- ); -}; - -const AssistantActionBar: FC = () => { - return ( - - - - s.message.isCopied}> - - - !s.message.isCopied}> - - - - - - - - - - - - - - - - - - - - Export as Markdown - - - - - - ); -}; - -const UserMessage: FC = () => { - return ( - - - -
-
- -
-
- -
-
- - -
- ); -}; - -const UserActionBar: FC = () => { - return ( - - - - - - - - ); -}; - -const EditComposer: FC = () => { - return ( - - - -
- - - - - - -
-
-
- ); -}; - -const BranchPicker: FC = ({ - className, - ...rest -}) => { - return ( - - - - - - - - / - - - - - - - - ); -}; diff --git a/frontend/src/components/assistant-ui/tool-fallback.tsx b/frontend/src/components/assistant-ui/tool-fallback.tsx index 9b664ba3..31756942 100644 --- a/frontend/src/components/assistant-ui/tool-fallback.tsx +++ b/frontend/src/components/assistant-ui/tool-fallback.tsx @@ -22,7 +22,7 @@ import { cn } from "@/lib/utils"; const ANIMATION_DURATION = 200; -export type ToolFallbackRootProps = Omit< +type ToolFallbackRootProps = Omit< React.ComponentProps, "open" | "onOpenChange" > & { @@ -315,10 +315,4 @@ ToolFallback.Error = ToolFallbackError; export { ToolFallback, - ToolFallbackRoot, - ToolFallbackTrigger, - ToolFallbackContent, - ToolFallbackArgs, - ToolFallbackResult, - ToolFallbackError, }; diff --git a/frontend/src/components/assistant-ui/tooltip-icon-button.tsx b/frontend/src/components/assistant-ui/tooltip-icon-button.tsx index 3209b01d..b4496985 100644 --- a/frontend/src/components/assistant-ui/tooltip-icon-button.tsx +++ b/frontend/src/components/assistant-ui/tooltip-icon-button.tsx @@ -11,7 +11,7 @@ import { import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -export type TooltipIconButtonProps = ComponentPropsWithRef & { +type TooltipIconButtonProps = ComponentPropsWithRef & { tooltip: string; side?: "top" | "bottom" | "left" | "right"; }; diff --git a/frontend/src/components/tool-ui/approval-card/index.tsx b/frontend/src/components/tool-ui/approval-card/index.tsx deleted file mode 100644 index 1bbfb6a6..00000000 --- a/frontend/src/components/tool-ui/approval-card/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export { ApprovalCard } from "./approval-card"; -export { - type SerializableApprovalCard, - type ApprovalCardProps, - type ApprovalDecision, - type MetadataItem, -} from "./schema"; diff --git a/frontend/src/components/tool-ui/approval-card/schema.ts b/frontend/src/components/tool-ui/approval-card/schema.ts index 9371e726..1645911c 100644 --- a/frontend/src/components/tool-ui/approval-card/schema.ts +++ b/frontend/src/components/tool-ui/approval-card/schema.ts @@ -1,14 +1,11 @@ import { z } from "zod"; import { ToolUIIdSchema, ToolUIRoleSchema } from "../shared/schema"; -import { defineToolUiContract } from "../shared/contract"; -export const MetadataItemSchema = z.object({ +const MetadataItemSchema = z.object({ key: z.string().min(1), value: z.string(), }); -export type MetadataItem = z.infer; - export const ApprovalDecisionSchema = z.enum(["approved", "denied"]); export type ApprovalDecision = z.infer; @@ -34,19 +31,6 @@ export type SerializableApprovalCard = z.infer< typeof SerializableApprovalCardSchema >; -const SerializableApprovalCardSchemaContract = defineToolUiContract( - "ApprovalCard", - SerializableApprovalCardSchema, -); - -export const parseSerializableApprovalCard: ( - input: unknown, -) => SerializableApprovalCard = SerializableApprovalCardSchemaContract.parse; - -export const safeParseSerializableApprovalCard: ( - input: unknown, -) => SerializableApprovalCard | null = - SerializableApprovalCardSchemaContract.safeParse; export interface ApprovalCardProps extends SerializableApprovalCard { className?: string; onConfirm?: () => void | Promise; diff --git a/frontend/src/components/tool-ui/code-block/code-block.tsx b/frontend/src/components/tool-ui/code-block/code-block.tsx index 76f19b5a..9cd24a86 100644 --- a/frontend/src/components/tool-ui/code-block/code-block.tsx +++ b/frontend/src/components/tool-ui/code-block/code-block.tsx @@ -142,7 +142,7 @@ function useResolvedTheme(): "light" | "dark" { return theme; } -export type CodeBlockRootProps = CodeBlockProps & { +type CodeBlockRootProps = CodeBlockProps & { children: ReactNode; expanded?: boolean; defaultExpanded?: boolean; @@ -348,7 +348,7 @@ function CodeBlockRoot({ ); } -export type CodeBlockSectionProps = { +type CodeBlockSectionProps = { className?: string; }; @@ -440,7 +440,7 @@ function CodeBlockCollapseToggle({ className }: CodeBlockSectionProps) { ); } -export type CodeBlockComposedProps = Omit; +type CodeBlockComposedProps = Omit; function CodeBlockComposed(props: CodeBlockComposedProps) { return ( diff --git a/frontend/src/components/tool-ui/code-block/index.tsx b/frontend/src/components/tool-ui/code-block/index.tsx deleted file mode 100644 index 9fd55b2f..00000000 --- a/frontend/src/components/tool-ui/code-block/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export { CodeBlock } from "./code-block"; -export type { - CodeBlockRootProps, - CodeBlockComposedProps, - CodeBlockSectionProps, -} from "./code-block"; -export type { - CodeBlockProps, - CodeBlockLineNumbersMode, - SerializableCodeBlock, -} from "./schema"; diff --git a/frontend/src/components/tool-ui/code-block/schema.ts b/frontend/src/components/tool-ui/code-block/schema.ts index 35c9b85b..c41bfb27 100644 --- a/frontend/src/components/tool-ui/code-block/schema.ts +++ b/frontend/src/components/tool-ui/code-block/schema.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { defineToolUiContract } from "../shared/contract"; import { ToolUIIdSchema, ToolUIReceiptSchema, @@ -20,24 +19,4 @@ export const CodeBlockPropsSchema = z.object({ }); export type CodeBlockProps = z.infer; -export type CodeBlockLineNumbersMode = CodeBlockProps["lineNumbers"]; - -export const SerializableCodeBlockSchema = CodeBlockPropsSchema.omit({ - className: true, -}); - -export type SerializableCodeBlock = z.infer; - -const SerializableCodeBlockSchemaContract = defineToolUiContract( - "CodeBlock", - SerializableCodeBlockSchema, -); - -export const parseSerializableCodeBlock: ( - input: unknown, -) => SerializableCodeBlock = SerializableCodeBlockSchemaContract.parse; - -export const safeParseSerializableCodeBlock: ( - input: unknown, -) => SerializableCodeBlock | null = - SerializableCodeBlockSchemaContract.safeParse; +export type CodeBlockLineNumbersMode = CodeBlockProps["lineNumbers"]; \ No newline at end of file diff --git a/frontend/src/components/tool-ui/code-diff/code-diff.tsx b/frontend/src/components/tool-ui/code-diff/code-diff.tsx index 38ab81bb..df2baa86 100644 --- a/frontend/src/components/tool-ui/code-diff/code-diff.tsx +++ b/frontend/src/components/tool-ui/code-diff/code-diff.tsx @@ -148,7 +148,7 @@ function useCodeDiff(): CodeDiffSharedState { /* ── Subcomponents ──────────────────────────────────────────────── */ -export type CodeDiffRootProps = CodeDiffProps & { +type CodeDiffRootProps = CodeDiffProps & { children: ReactNode; expanded?: boolean; defaultExpanded?: boolean; @@ -295,7 +295,7 @@ function CodeDiffRoot({ ); } -export type CodeDiffSectionProps = { +type CodeDiffSectionProps = { className?: string; }; @@ -434,7 +434,7 @@ function CodeDiffCollapseToggle({ className }: CodeDiffSectionProps) { /* ── Composed preset (callable as a flat component) ─────────────── */ -export type CodeDiffComposedProps = Omit; +type CodeDiffComposedProps = Omit; function CodeDiffComposed(props: CodeDiffComposedProps) { return ( diff --git a/frontend/src/components/tool-ui/code-diff/index.tsx b/frontend/src/components/tool-ui/code-diff/index.tsx deleted file mode 100644 index 5c39834c..00000000 --- a/frontend/src/components/tool-ui/code-diff/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export { CodeDiff } from "./code-diff"; -export type { - CodeDiffRootProps, - CodeDiffComposedProps, - CodeDiffSectionProps, -} from "./code-diff"; -export type { CodeDiffProps, SerializableCodeDiff } from "./schema"; diff --git a/frontend/src/components/tool-ui/code-diff/schema.ts b/frontend/src/components/tool-ui/code-diff/schema.ts index a480cded..176a1f9c 100644 --- a/frontend/src/components/tool-ui/code-diff/schema.ts +++ b/frontend/src/components/tool-ui/code-diff/schema.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { defineToolUiContract } from "../shared/contract"; import { ToolUIIdSchema, ToolUIReceiptSchema, @@ -50,22 +49,3 @@ export const CodeDiffPropsSchema = CodeDiffPropsSchemaBase.superRefine( ); export type CodeDiffProps = z.infer; - -export const SerializableCodeDiffSchema = CodeDiffPropsSchemaBase.omit({ - className: true, -}).superRefine(validateCodeDiffInputMode); - -export type SerializableCodeDiff = z.infer; - -const SerializableCodeDiffSchemaContract = defineToolUiContract( - "CodeDiff", - SerializableCodeDiffSchema, -); - -export const parseSerializableCodeDiff: ( - input: unknown, -) => SerializableCodeDiff = SerializableCodeDiffSchemaContract.parse; - -export const safeParseSerializableCodeDiff: ( - input: unknown, -) => SerializableCodeDiff | null = SerializableCodeDiffSchemaContract.safeParse; diff --git a/frontend/src/components/tool-ui/data-table/README.md b/frontend/src/components/tool-ui/data-table/README.md deleted file mode 100644 index 706d01a8..00000000 --- a/frontend/src/components/tool-ui/data-table/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Data Table - -Implementation for the "data-table" Tool UI surface. - -## Files - -- public exports: components/tool-ui/data-table/index.tsx -- serializable schema + parse helpers: components/tool-ui/data-table/schema.ts - -## Companion assets - -- Docs page: app/docs/data-table/content.mdx -- Preset payload: lib/presets/data-table.ts - -## Quick check - -Run this after edits: - -pnpm test diff --git a/frontend/src/components/tool-ui/data-table/_adapter.tsx b/frontend/src/components/tool-ui/data-table/_adapter.tsx deleted file mode 100644 index d6dc7f71..00000000 --- a/frontend/src/components/tool-ui/data-table/_adapter.tsx +++ /dev/null @@ -1,23 +0,0 @@ -export { cn } from "@/lib/utils"; -export { Button } from "@/components/ui/button"; -export { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; -export { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -export { Badge } from "@/components/ui/badge"; -export { - Table, - TableHeader, - TableBody, - TableHead, - TableRow, - TableCell, -} from "@/components/ui/table"; diff --git a/frontend/src/components/tool-ui/data-table/data-table.tsx b/frontend/src/components/tool-ui/data-table/data-table.tsx deleted file mode 100644 index 415b8ab1..00000000 --- a/frontend/src/components/tool-ui/data-table/data-table.tsx +++ /dev/null @@ -1,936 +0,0 @@ -"use client"; - -import * as React from "react"; -import { - cn, - Table, - TableBody, - TableRow, - TableCell, - TableHeader, - TableHead, - Button, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "./_adapter"; -import { - sortData, - createDataTableRowKeys, - getDataTableMobileDescriptionId, -} from "./utilities"; -import { renderFormattedValue } from "./formatters"; -import type { - DataTableProps, - DataTableContextValue, - RowData, - DataTableRowData, - ColumnKey, - Column, -} from "./types"; -import type { FormatConfig } from "./formatters"; - -const DEFAULT_LOCALE = "en-US" as const; - -function isNumericFormat(format?: FormatConfig): boolean { - const kind = format?.kind; - return ( - kind === "number" || - kind === "currency" || - kind === "percent" || - kind === "delta" - ); -} - -function getAlignmentClass( - align?: "left" | "right" | "center", -): string | undefined { - if (align === "right") return "text-right"; - if (align === "center") return "text-center"; - return undefined; -} - -const DataTableContext = React.createContext< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - DataTableContextValue | undefined ->(undefined); - -function useDataTable() { - const context = React.useContext(DataTableContext) as - | DataTableContextValue - | undefined; - if (!context) { - throw new Error("useDataTable must be used within "); - } - return context; -} - -type DataTableLayout = "auto" | "table" | "cards"; - -type DataTableBaseProps = DataTableProps & { - layout: DataTableLayout; -}; - -type DataTableProviderProps = Pick< - DataTableProps, - | "columns" - | "data" - | "rowIdKey" - | "defaultSort" - | "sort" - | "onSortChange" - | "id" - | "locale" -> & { - children: React.ReactNode; -}; - -function DataTableProvider({ - columns, - data: rawData, - rowIdKey, - defaultSort, - sort: controlledSort, - id, - onSortChange, - locale, - children, -}: DataTableProviderProps) { - // Default locale avoids SSR/client formatting mismatches. - const resolvedLocale = locale ?? DEFAULT_LOCALE; - - const [internalSortBy, setInternalSortBy] = React.useState< - ColumnKey | undefined - >(defaultSort?.by); - const [internalSortDirection, setInternalSortDirection] = React.useState< - "asc" | "desc" | undefined - >(defaultSort?.direction); - - const sortBy = controlledSort?.by ?? internalSortBy; - const sortDirection = controlledSort?.direction ?? internalSortDirection; - - const data = React.useMemo(() => { - if (!sortBy || !sortDirection) return rawData; - return sortData(rawData, sortBy, sortDirection, resolvedLocale); - }, [rawData, sortBy, sortDirection, resolvedLocale]); - - const handleSort = React.useCallback( - (key: ColumnKey) => { - let newDirection: "asc" | "desc" | undefined; - - if (sortBy === key) { - if (sortDirection === "asc") { - newDirection = "desc"; - } else if (sortDirection === "desc") { - newDirection = undefined; - } else { - newDirection = "asc"; - } - } else { - newDirection = "asc"; - } - - const next = { - by: newDirection ? key : undefined, - direction: newDirection, - } as const; - - if (controlledSort) { - onSortChange?.(next); - } else { - setInternalSortBy(next.by); - setInternalSortDirection(next.direction); - } - }, - [sortBy, sortDirection, controlledSort, onSortChange], - ); - - const contextValue: DataTableContextValue = { - columns, - data, - rowIdKey, - sortBy, - sortDirection, - toggleSort: handleSort, - id, - locale: resolvedLocale, - }; - - return ( - - {children} - - ); -} - -interface DataTableLayoutProps { - layout: DataTableLayout; - emptyMessage: string; - maxHeight?: string; - className?: string; -} - -function DataTableLayout({ - layout, - emptyMessage, - maxHeight, - className, -}: DataTableLayoutProps) { - const { columns, data, rowIdKey, sortBy, sortDirection, id } = useDataTable(); - const rowKeys = React.useMemo( - () => - createDataTableRowKeys( - data as Array>, - rowIdKey ? String(rowIdKey) : undefined, - ), - [data, rowIdKey], - ); - const mobileDescriptionId = React.useMemo( - () => getDataTableMobileDescriptionId(String(id ?? "data-table")), - [id], - ); - - const sortAnnouncement = React.useMemo(() => { - const col = columns.find((c) => c.key === sortBy); - const label = col?.label ?? sortBy; - return sortBy && sortDirection - ? `Sorted by ${label}, ${sortDirection === "asc" ? "ascending" : "descending"}` - : ""; - }, [columns, sortBy, sortDirection]); - - return ( -
-
-
-
- - {columns.length > 0 && ( - - {columns.map((col) => ( - - ))} - - )} - {data.length === 0 ? ( - - ) : ( - - )} -
-
-
-
- -
-
- Table data shown as expandable cards. Each card represents one row. - {columns.length > 0 && - ` Columns: ${columns.map((c) => c.label).join(", ")}.`} -
- - {data.length === 0 ? ( -
- {emptyMessage} -
- ) : ( -
- {data.map((row, i) => { - const rowKey = rowKeys[i]; - return ( - - ); - })} -
- )} -
- - {sortAnnouncement && ( -
- {sortAnnouncement} -
- )} -
- ); -} - -function DataTableBase( - props: DataTableBaseProps, -) { - const { - columns, - data, - rowIdKey, - defaultSort, - sort, - onSortChange, - id, - locale, - layout, - emptyMessage = "No data available", - maxHeight, - className, - } = props; - - return ( - - - - ); -} - -function DataTableRoot(props: DataTableProps) { - return ; -} - -function DataTableTable(props: DataTableProps) { - return ; -} - -function DataTableCards(props: DataTableProps) { - return ; -} - -type DataTableComponent = { - (props: DataTableProps): React.ReactElement; - Table: typeof DataTableTable; - Cards: typeof DataTableCards; - Provider: typeof DataTableProvider; -}; - -export const DataTable = Object.assign(DataTableRoot, { - Table: DataTableTable, - Cards: DataTableCards, - Provider: DataTableProvider, -}) as DataTableComponent; - -function DataTableContent() { - return ( - <> - - - - ); -} - -function DataTableEmpty({ message }: { message: string }) { - const { columns } = useDataTable(); - - return ( - - - - {message} - - - - ); -} - -function SortIcon({ state }: { state?: "asc" | "desc" }) { - let char = "⇅"; - let className = "opacity-20"; - - if (state === "asc") { - char = "↑"; - className = ""; - } - - if (state === "desc") { - char = "↓"; - className = ""; - } - - return ( - - {char} - - ); -} - -function DataTableHeader() { - const { columns } = useDataTable(); - - return ( - - - - {columns.map((column, columnIndex) => ( - - ))} - - - - ); -} - -interface DataTableHeadProps { - column: Column; - columnIndex?: number; - totalColumns?: number; -} - -function DataTableHead({ - column, - columnIndex = 0, - totalColumns = 1, -}: DataTableHeadProps) { - const { sortBy, sortDirection, toggleSort } = useDataTable(); - const isFirstColumn = columnIndex === 0; - const isLastColumn = columnIndex === totalColumns - 1; - - const isSortable = column.sortable !== false; - - const isSorted = sortBy === column.key; - const direction = isSorted ? sortDirection : undefined; - const isDisabled = !isSortable; - - const handleClick = () => { - if (!isDisabled && toggleSort) { - toggleSort(column.key); - } - }; - - const displayText = column.abbr || column.label; - const shouldShowTooltip = column.abbr || displayText.length > 15; - const isNumericKind = isNumericFormat(column.format); - const align = - column.align ?? - (columnIndex === 0 ? "left" : isNumericKind ? "right" : "left"); - const alignClass = getAlignmentClass(align); - const buttonAlignClass = cn( - "min-w-0 gap-1 font-normal", - align === "right" && "text-right", - align === "center" && "text-center", - align === "left" && "text-left", - ); - const labelAlignClass = - align === "right" - ? "text-right" - : align === "center" - ? "text-center" - : "text-left"; - - return ( - - - - ); -} - -function DataTableBody() { - const { data, rowIdKey } = useDataTable(); - const rowKeys = React.useMemo( - () => - createDataTableRowKeys( - data as Array>, - rowIdKey ? String(rowIdKey) : undefined, - ), - [data, rowIdKey], - ); - const hasWarnedRowKeyRef = React.useRef(false); - - React.useEffect(() => { - if (hasWarnedRowKeyRef.current) return; - if (process.env.NODE_ENV !== "production" && !rowIdKey && data.length > 0) { - hasWarnedRowKeyRef.current = true; - console.warn( - "[DataTable] Missing `rowIdKey` prop. Falling back to inferred/content-derived row keys. " + - "Strongly recommended: Pass a `rowIdKey` prop that points to a unique identifier in your row data (e.g., 'id', 'uuid', 'symbol').\n" + - 'Example: ', - ); - } - }, [rowIdKey, data.length]); - - return ( - - {data.map((row, index) => { - const rowKey = rowKeys[index]; - return ; - })} - - ); -} - -interface DataTableRowProps { - row: DataTableRowData; - className?: string; -} - -function DataTableRow({ row, className }: DataTableRowProps) { - const { columns } = useDataTable(); - - return ( - - {columns.map((column, columnIndex) => ( - - ))} - - ); -} - -interface DataTableCellProps { - value: - | string - | number - | boolean - | null - | (string | number | boolean | null)[]; - column: Column; - row: DataTableRowData; - className?: string; - columnIndex?: number; -} - -function DataTableCell({ - value, - column, - row, - className, - columnIndex = 0, -}: DataTableCellProps) { - const { locale } = useDataTable(); - const isNumericKind = isNumericFormat(column.format); - const isNumericValue = typeof value === "number"; - const displayValue = renderFormattedValue({ value, column, row, locale }); - const align = - column.align ?? - (columnIndex === 0 - ? "left" - : isNumericKind || isNumericValue - ? "right" - : "left"); - const alignClass = getAlignmentClass(align); - - return ( - - {displayValue} - - ); -} - -function categorizeColumns(columns: Column[]) { - const primary: Column[] = []; - const secondary: Column[] = []; - - let visibleColumnCount = 0; - columns.forEach((col) => { - if (col.hideOnMobile) return; - - if (col.priority === "primary") { - primary.push(col); - } else if (col.priority === "secondary") { - secondary.push(col); - } else if (col.priority === "tertiary") { - return; - } else { - if (visibleColumnCount < 2) { - primary.push(col); - } else { - secondary.push(col); - } - visibleColumnCount++; - } - }); - - return { primary, secondary }; -} - -interface DataTableAccordionCardProps { - row: DataTableRowData; - index: number; - rowKey: string; - isFirst?: boolean; -} - -function getDataTableRowDomId(rowKey: string): string { - return encodeURIComponent(rowKey).replace(/%/g, "_"); -} - -function DataTableAccordionCard({ - row, - index, - rowKey, - isFirst = false, -}: DataTableAccordionCardProps) { - const { columns, locale } = useDataTable(); - - const { primary, secondary } = React.useMemo( - () => categorizeColumns(columns), - [columns], - ); - - if (secondary.length === 0) { - return ( - - ); - } - - const primaryColumn = primary[0]; - const remainingPrimaryColumns = primary.slice(1); - - const stableRowId = getDataTableRowDomId(rowKey); - - const headingId = `row-${stableRowId}-heading`; - const detailsId = `row-${stableRowId}-details`; - const remainingPrimaryDataIds = remainingPrimaryColumns.map( - (col) => `row-${stableRowId}-${String(col.key)}`, - ); - - const primaryValue = primaryColumn - ? String(row[primaryColumn.key] ?? "") - : ""; - const rowLabel = `Row ${index + 1}: ${primaryValue}`; - const accordionItemId = `row-${stableRowId}`; - - return ( - - - 0 ? "Expand for details" : ""}`} - > -
- {primaryColumn && ( -
- {renderFormattedValue({ - value: row[primaryColumn.key], - column: primaryColumn, - row, - locale, - })} -
- )} - - {remainingPrimaryColumns.length > 0 && ( -
- {remainingPrimaryColumns.map((col, idx) => ( - - {col.label}: - - - {renderFormattedValue({ - value: row[col.key], - column: col, - row, - locale, - })} - - - ))} -
- )} -
-
- - - {secondary.length > 0 && ( -
- {secondary.map((col) => ( -
-
- {col.label} -
-
- {renderFormattedValue({ - value: row[col.key], - column: col, - row, - locale, - })} -
-
- ))} -
- )} -
-
-
- ); -} - -/** - * Simple card with no accordion, for when there are only primary columns - */ -function SimpleCard({ - row, - columns, - index, - rowKey, - isFirst = false, -}: { - row: DataTableRowData; - columns: Column[]; - index: number; - rowKey: string; - isFirst?: boolean; -}) { - const { locale } = useDataTable(); - const primaryColumn = columns[0]; - const otherColumns = columns.slice(1); - - const stableRowId = getDataTableRowDomId(rowKey); - - const primaryValue = primaryColumn - ? String(row[primaryColumn.key] ?? "") - : ""; - const rowLabel = `Row ${index + 1}: ${primaryValue}`; - - return ( -
- {primaryColumn && ( -
- {renderFormattedValue({ - value: row[primaryColumn.key], - column: primaryColumn, - row, - locale, - })} -
- )} - - {otherColumns.map((col) => ( -
- - {col.label}: - - - {renderFormattedValue({ - value: row[col.key], - column: col, - row, - locale, - })} - -
- ))} -
- ); -} diff --git a/frontend/src/components/tool-ui/data-table/formatters.tsx b/frontend/src/components/tool-ui/data-table/formatters.tsx deleted file mode 100644 index ed6515c2..00000000 --- a/frontend/src/components/tool-ui/data-table/formatters.tsx +++ /dev/null @@ -1,514 +0,0 @@ -"use client"; - -import * as React from "react"; -import { cn, Badge, Tooltip, TooltipContent, TooltipTrigger } from "./_adapter"; - -function sanitizeHref(href?: string): string | undefined { - if (!href) return undefined; - const candidate = href.trim(); - if (!candidate) return undefined; - - if ( - candidate.startsWith("/") || - candidate.startsWith("./") || - candidate.startsWith("../") || - candidate.startsWith("?") || - candidate.startsWith("#") - ) { - if (candidate.startsWith("//")) return undefined; - // eslint-disable-next-line no-control-regex -- intentionally matching control characters - if (/[\u0000-\u001F\u007F]/.test(candidate)) return undefined; - return candidate; - } - - try { - const url = new URL(candidate); - if (url.protocol === "http:" || url.protocol === "https:") { - return url.toString(); - } - } catch { - return undefined; - } - return undefined; -} - -function resolveSafeNavigationHref( - ...candidates: Array -): string | undefined { - for (const candidate of candidates) { - const safeHref = sanitizeHref(candidate ?? undefined); - if (safeHref) { - return safeHref; - } - } - - return undefined; -} - -type Tone = "success" | "warning" | "danger" | "info" | "neutral"; - -export type FormatConfig = - | { kind: "text" } - | { - kind: "number"; - decimals?: number; - unit?: string; - compact?: boolean; - showSign?: boolean; - } - | { kind: "currency"; currency: string; decimals?: number } - | { - kind: "percent"; - decimals?: number; - showSign?: boolean; - basis?: "fraction" | "unit"; - } - | { kind: "date"; dateFormat?: "short" | "long" | "relative" } - | { - kind: "delta"; - decimals?: number; - upIsPositive?: boolean; - showSign?: boolean; - } - | { - kind: "status"; - statusMap: Record; - } - | { kind: "boolean"; labels?: { true: string; false: string } } - | { kind: "link"; hrefKey?: string; external?: boolean } - | { kind: "badge"; colorMap?: Record } - | { kind: "array"; maxVisible?: number }; - -interface DeltaValueProps { - value: number; - options?: Extract; - locale?: string; -} - -function DeltaValue({ value, options, locale }: DeltaValueProps) { - const decimals = options?.decimals ?? 2; - const upIsPositive = options?.upIsPositive ?? true; - const showSign = options?.showSign ?? true; - - const isPositive = value > 0; - const isNegative = value < 0; - const isNeutral = value === 0; - - const isGood = upIsPositive ? isPositive : isNegative; - const isBad = upIsPositive ? isNegative : isPositive; - - const colorClass = isGood - ? "text-green-700 dark:text-green-500" - : isBad - ? "text-destructive" - : "text-muted-foreground"; - - const absValue = Math.abs(value); - const formatted = new Intl.NumberFormat(locale, { - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - }).format(absValue); - - const display = - showSign && !isNeutral - ? isNegative - ? `-${formatted}` - : `+${formatted}` - : formatted; - - const arrow = isPositive ? "↑" : isNegative ? "↓" : ""; - - return ( - - {display} - {!isNeutral && {arrow}} - - ); -} - -interface StatusBadgeProps { - value: string; - options?: Extract; -} - -function StatusBadge({ value, options }: StatusBadgeProps) { - const config = options?.statusMap?.[value] ?? { - tone: "neutral" as Tone, - label: value, - }; - const label = config.label ?? value; - - const variant = - config.tone === "danger" - ? "destructive" - : config.tone === "neutral" - ? "outline" - : "secondary"; - - return ( - - {label} - - ); -} - -interface CurrencyValueProps { - value: number; - options?: Extract; - locale?: string; -} - -function CurrencyValue({ value, options, locale }: CurrencyValueProps) { - const currency = options?.currency ?? "USD"; - const decimals = options?.decimals ?? 2; - - const formatted = new Intl.NumberFormat(locale, { - style: "currency", - currency, - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - }).format(value); - - return {formatted}; -} - -interface PercentValueProps { - value: number; - options?: Extract; - locale?: string; -} - -function PercentValue({ value, options, locale }: PercentValueProps) { - const decimals = options?.decimals ?? 2; - const showSign = options?.showSign ?? false; - const basis = options?.basis ?? "fraction"; - - const numeric = basis === "fraction" ? value : value / 100; - - const formatted = new Intl.NumberFormat(locale, { - style: "percent", - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - signDisplay: showSign ? "always" : "auto", - }).format(numeric); - - return {formatted}; -} - -interface DateValueProps { - value: string; - options?: Extract; - locale?: string; -} - -function DateValue({ value, options, locale }: DateValueProps) { - const dateFormat = options?.dateFormat ?? "short"; - const date = new Date(value); - - if (isNaN(date.getTime())) { - return {value}; - } - - let formatted: string; - - if (dateFormat === "relative") { - formatted = getRelativeTime(date, locale); - } else if (dateFormat === "long") { - formatted = new Intl.DateTimeFormat(locale, { - year: "numeric", - month: "long", - day: "numeric", - }).format(date); - } else { - formatted = new Intl.DateTimeFormat(locale, { - year: "numeric", - month: "short", - day: "numeric", - }).format(date); - } - - const title = new Intl.DateTimeFormat(locale, { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }).format(date); - - return ( - - {formatted} - - ); -} - -function getRelativeTime(date: Date, locale?: string): string { - const now = new Date(); - const diffInSeconds = Math.trunc((date.getTime() - now.getTime()) / 1000); - const absDiffInSeconds = Math.abs(diffInSeconds); - - if (absDiffInSeconds < 60) return "just now"; - - const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); - - if (absDiffInSeconds < 3600) { - const mins = Math.trunc(diffInSeconds / 60); - return rtf.format(mins, "minute"); - } - if (absDiffInSeconds < 86400) { - const hours = Math.trunc(diffInSeconds / 3600); - return rtf.format(hours, "hour"); - } - if (absDiffInSeconds < 604800) { - const days = Math.trunc(diffInSeconds / 86400); - return rtf.format(days, "day"); - } - - return new Intl.DateTimeFormat(locale, { - year: "numeric", - month: "short", - day: "numeric", - }).format(date); -} - -interface BooleanValueProps { - value: boolean; - options?: Extract; -} - -function BooleanValue({ value, options }: BooleanValueProps) { - const labels = options?.labels ?? { true: "Yes", false: "No" }; - const label = value ? labels.true : labels.false; - const variant = value ? "secondary" : "outline"; - - return {label}; -} - -interface LinkValueProps { - value: string; - options?: Extract; - row?: Record< - string, - string | number | boolean | null | (string | number | boolean | null)[] - >; -} - -function LinkValue({ value, options, row }: LinkValueProps) { - const rawHref = - options?.hrefKey && row ? String(row[options.hrefKey] ?? "") : value; - const href = resolveSafeNavigationHref(rawHref); - const external = options?.external ?? false; - - if (!href) { - return {value}; - } - - return ( - e.stopPropagation()} - > - {value} - {external && ( - - ↗ - - )} - - ); -} - -interface NumberValueProps { - value: number; - options?: Extract; - locale?: string; -} - -function NumberValue({ value, options, locale }: NumberValueProps) { - const decimals = options?.decimals ?? 0; - const unit = options?.unit ?? ""; - const compact = options?.compact ?? false; - const showSign = options?.showSign ?? false; - - const formatted = new Intl.NumberFormat(locale, { - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - notation: compact ? "compact" : "standard", - }).format(value); - - const display = showSign && value > 0 ? `+${formatted}` : formatted; - - return ( - - {display} - {unit} - - ); -} - -interface BadgeValueProps { - value: string; - options?: Extract; -} - -function BadgeValue({ value, options }: BadgeValueProps) { - const tone = options?.colorMap?.[value] ?? "neutral"; - - const variant = - tone === "danger" - ? "destructive" - : tone === "neutral" - ? "outline" - : "secondary"; - - return ( - - {value} - - ); -} - -interface ArrayValueProps { - value: (string | number | boolean | null)[] | string; - options?: Extract; -} - -function ArrayValue({ value, options }: ArrayValueProps) { - const maxVisible = options?.maxVisible ?? 3; - const items: (string | number | boolean | null)[] = Array.isArray(value) - ? value - : typeof value === "string" - ? value.split(",").map((s) => s.trim()) - : []; - - if (items.length === 0) { - return ; - } - - const visible = items.slice(0, maxVisible); - const remaining = items.length - maxVisible; - - const hidden = items.slice(maxVisible); - - return ( - - {visible.map((item, i) => ( - - {item === null ? "null" : String(item)} - - ))} - {remaining > 0 && ( - - - - +{remaining} more - - - - {hidden - .map((item) => (item === null ? "null" : String(item))) - .join(", ")} - - - )} - - ); -} - -interface RenderFormattedValueParams { - value: - | string - | number - | boolean - | null - | (string | number | boolean | null)[]; - column: { format?: FormatConfig }; - row?: Record< - string, - string | number | boolean | null | (string | number | boolean | null)[] - >; - locale?: string; -} - -export function renderFormattedValue({ - value, - column, - row, - locale, -}: RenderFormattedValueParams): React.ReactNode { - if (value == null || value === "") { - return ; - } - - const fmt = column.format; - - switch (fmt?.kind) { - case "delta": - return ; - case "status": - return ; - case "currency": - return ( - - ); - case "percent": - return ( - - ); - case "date": - return ; - case "boolean": - return ; - case "link": - return ; - case "number": - return ( - - ); - case "badge": - return ; - case "array": - return ( - - ); - case "text": - default: - return String(value); - } -} diff --git a/frontend/src/components/tool-ui/data-table/types.ts b/frontend/src/components/tool-ui/data-table/types.ts deleted file mode 100644 index e8902803..00000000 --- a/frontend/src/components/tool-ui/data-table/types.ts +++ /dev/null @@ -1,262 +0,0 @@ -import type { ToolUIId, ToolUIReceipt, ToolUIRole } from "../shared/schema"; -import type { FormatConfig } from "./formatters"; - -/** - * JSON primitive type that can be serialized. - */ -type JsonPrimitive = string | number | boolean | null; - -/** - * Valid row value types for serializable DataTable data. - * - * Supports: - * - Primitives: string, number, boolean, null - * - Arrays of primitives: string[], number[], boolean[], or mixed primitive arrays - * - * For complex data (objects with href/label, etc.), use column format configs - * instead of putting objects in row data. - * - * @example - * ```ts - * // 👍 Good: Use primitives and primitive arrays - * const row = { - * name: "Widget", - * price: 29.99, - * tags: ["electronics", "featured"], - * metrics: [1.2, 3.4, 5.6] - * } - * - * // 🚫 Bad: Don't put objects in row data - * const row = { - * link: { href: "/path", label: "Click" } // Use format: { kind: 'link' } instead - * } - * ``` - */ -export type RowPrimitive = JsonPrimitive | JsonPrimitive[]; -export type DataTableRowData = Record; -export type RowData = Record; -export type ColumnKey = Extract; - -export type FormatFor = V extends number - ? Extract - : V extends boolean - ? Extract - : V extends (string | number | boolean | null)[] - ? Extract - : V extends string - ? Extract< - FormatConfig, - { kind: "text" | "link" | "date" | "badge" | "status" } - > - : Extract; - -/** - * Column definition for DataTable - * - * @remarks - * **Important:** Columns are sortable by default (opt-out pattern). - * Set `sortable: false` explicitly to disable sorting for specific columns. - */ -export interface Column< - T extends object = DataTableRowData, - K extends ColumnKey = ColumnKey, -> { - /** Unique identifier that maps to a key in the row data */ - key: K; - /** Display text for the column header */ - label: string; - /** Abbreviated label for narrow viewports */ - abbr?: string; - /** Whether column is sortable. Default: true (opt-out pattern) */ - sortable?: boolean; - /** Text alignment for column cells */ - align?: "left" | "right" | "center"; - /** Optional fixed width (CSS value) */ - width?: string; - /** Enable text truncation with ellipsis */ - truncate?: boolean; - /** Mobile display priority (primary = always visible, secondary = expandable, tertiary = hidden) */ - priority?: "primary" | "secondary" | "tertiary"; - /** Completely hide column on mobile viewports */ - hideOnMobile?: boolean; - /** Formatting configuration for cell values */ - format?: FormatFor; -} - -/** - * Serializable props that can come from LLM tool calls or be JSON-serialized. - * - * These props contain only primitive values, arrays, and plain objects - - * no functions, class instances, or other non-serializable values. - * - * @example - * ```tsx - * const serializableProps: DataTableSerializableProps = { - * columns: [...], - * data: [...], - * rowIdKey: "id", - * defaultSort: { by: "price", direction: "desc" } - * } - * ``` - */ -export interface DataTableSerializableProps { - /** - * Unique identifier for this tool UI instance in the conversation. - * - * Used for: - * - Assistant referencing ("the table above") - * - Receipt generation (linking actions to their source) - * - Narration context - * - * Should be stable across re-renders, meaningful, and unique within the conversation. - * - * @example "data-table-expenses-q3", "search-results-repos" - */ - id: ToolUIId; - /** Optional surface role metadata (serializable) */ - role?: ToolUIRole; - /** Optional receipt metadata for consequential outcomes (serializable) */ - receipt?: ToolUIReceipt; - /** Column definitions */ - columns: Column[]; - /** Row data (primitives only - no functions or class instances) */ - data: T[]; - /** - * Key in row data to use as unique identifier for React keys - * - * **Strongly recommended:** Always provide this for dynamic data to prevent - * reconciliation issues (focus traps, animation glitches, incorrect state preservation) - * when data reorders. Falls back to array index if omitted (only acceptable for static mock data). - * - * @example rowIdKey="id" or rowIdKey="uuid" - */ - rowIdKey?: ColumnKey; - /** - * Uncontrolled initial sort state (table manages its own sort state internally) - * - * **Sorting cycle:** Clicking column headers cycles through tri-state: - * 1. none (unsorted) → 2. asc → 3. desc → 4. none (back to unsorted) - * - * @example - * ```tsx - * // Start with descending price sort - * - * ``` - */ - defaultSort?: { by?: ColumnKey; direction?: "asc" | "desc" }; - /** - * Controlled sort state (use with onSortChange from client props) - * - * When provided, you must also provide `onSortChange` to handle sort updates. - * The table will cycle through: none → asc → desc → none. - * - * @example - * ```tsx - * const [sort, setSort] = useState({ by: "price", direction: "desc" }) - * - * ``` - */ - sort?: { by?: ColumnKey; direction?: "asc" | "desc" }; - /** Empty state message */ - emptyMessage?: string; - /** Max table height with vertical scroll (CSS value) */ - maxHeight?: string; - /** - * BCP47 locale for formatting and sorting (e.g., 'en-US', 'de-DE', 'ja-JP') - * - * Defaults to 'en-US' to ensure consistent server/client rendering. - * Pass explicit locale for internationalization. - * - * @example - * ```tsx - * // German formatting - * // Japanese formatting - * // Uses 'en-US' default - * ``` - */ - locale?: string; -} - -/** - * Client-side React-only props that cannot be serialized. - * - * These props contain functions, component state, or other React-specific values - * that must be provided by your React code (not from LLM tool calls). - * - * @example - * ```tsx - * const clientProps: DataTableClientProps = { - * className: "my-table", - * onSortChange: (next) => setSort(next), - * // Compose local/decision actions externally via LocalActions/DecisionActions - * } - * ``` - */ -export interface DataTableClientProps { - /** Additional CSS classes */ - className?: string; - /** - * Sort change handler for controlled mode (required if sort is provided) - * - * **Tri-state cycle behavior:** - * - Click unsorted column: `{ by: "column", direction: "asc" }` - * - Click asc column: `{ by: "column", direction: "desc" }` - * - Click desc column: `{ by: "column", direction: undefined }` (returns to unsorted) - * - Click different column: `{ by: "newColumn", direction: "asc" }` - * - * @example - * ```tsx - * const [sort, setSort] = useState<{ by?: string; direction?: "asc" | "desc" }>({}) - * - * { - * console.log("Sort changed:", next) - * setSort(next) - * }} - * /> - * ``` - */ - onSortChange?: (next: { - by?: ColumnKey; - direction?: "asc" | "desc"; - }) => void; -} - -/** - * Complete props for the DataTable component. - * - * Combines serializable props (can come from LLM tool calls) with client-side - * React-only props. This separation makes the boundary explicit and prevents - * accidental serialization of non-serializable values. - * - * @see {@link DataTableSerializableProps} for props that can be JSON-serialized - * @see {@link DataTableClientProps} for React-only props - * @see {@link parseSerializableDataTable} for parsing LLM tool call results - * - * @example - * ```tsx - * // From LLM tool call - * const serializableProps = parseSerializableDataTable(llmResult) - * - * // Combine with React-specific props - * - * ``` - */ -export interface DataTableProps - extends DataTableSerializableProps, DataTableClientProps {} - -export interface DataTableContextValue { - columns: Column[]; - data: T[]; - rowIdKey?: ColumnKey; - sortBy?: ColumnKey; - sortDirection?: "asc" | "desc"; - toggleSort?: (key: ColumnKey) => void; - id?: string; - locale?: string; -} diff --git a/frontend/src/components/tool-ui/data-table/utilities.ts b/frontend/src/components/tool-ui/data-table/utilities.ts deleted file mode 100644 index ee4116dc..00000000 --- a/frontend/src/components/tool-ui/data-table/utilities.ts +++ /dev/null @@ -1,296 +0,0 @@ -export function sortData>( - data: T[], - key: K, - direction: "asc" | "desc", - locale?: string, -): T[] { - const get = (obj: T, k: K): unknown => (obj as Record)[k]; - const collator = new Intl.Collator(locale, { - numeric: true, - sensitivity: "base", - }); - return [...data].sort((a, b) => { - const aVal = get(a, key); - const bVal = get(b, key); - - // Handle nulls - if (aVal == null && bVal == null) return 0; - if (aVal == null) return 1; - if (bVal == null) return -1; - - // Type-specific comparison - // Numbers - if (typeof aVal === "number" && typeof bVal === "number") { - return direction === "asc" ? aVal - bVal : bVal - aVal; - } - // Dates (Date instances) - if (aVal instanceof Date && bVal instanceof Date) { - const diff = aVal.getTime() - bVal.getTime(); - return direction === "asc" ? diff : -diff; - } - // Booleans: false < true - if (typeof aVal === "boolean" && typeof bVal === "boolean") { - const diff = aVal === bVal ? 0 : aVal ? 1 : -1; - return direction === "asc" ? diff : -diff; - } - // Arrays: compare length - if (Array.isArray(aVal) && Array.isArray(bVal)) { - const diff = aVal.length - bVal.length; - return direction === "asc" ? diff : -diff; - } - // Strings that look like numbers -> numeric compare - if (typeof aVal === "string" && typeof bVal === "string") { - const numA = parseNumericLike(aVal); - const numB = parseNumericLike(bVal); - if (numA != null && numB != null) { - const diff = numA - numB; - return direction === "asc" ? diff : -diff; - } - // ISO-like date strings - if (/^\d{4}-\d{2}-\d{2}/.test(aVal) && /^\d{4}-\d{2}-\d{2}/.test(bVal)) { - const da = new Date(aVal).getTime(); - const db = new Date(bVal).getTime(); - const diff = da - db; - return direction === "asc" ? diff : -diff; - } - } - - // Fallback: locale-aware string compare with numeric collation - const aStr = String(aVal); - const bStr = String(bVal); - const comparison = collator.compare(aStr, bStr); - return direction === "asc" ? comparison : -comparison; - }); -} - -/** - * Return a human-friendly identifier for a row using common keys - * - * Accepts any JSON-serializable primitive or array of primitives. - * Arrays are converted to comma-separated strings. - */ -function getRowIdentifier( - row: Record< - string, - string | number | boolean | null | (string | number | boolean | null)[] - >, - identifierKey?: string, -): string { - const candidate = - (identifierKey ? row[identifierKey] : undefined) ?? - (row as Record).name ?? - (row as Record).title ?? - (row as Record).id; - - if (candidate == null) { - return ""; - } - - // Handle arrays by joining them - if (Array.isArray(candidate)) { - return candidate.map((v) => (v === null ? "null" : String(v))).join(", "); - } - - return String(candidate).trim(); -} - -function stableStringify(value: unknown): string { - if (value == null) return "null"; - if (typeof value === "string") return JSON.stringify(value); - if ( - typeof value === "number" || - typeof value === "boolean" || - typeof value === "bigint" - ) { - return String(value); - } - if (Array.isArray(value)) { - return `[${value.map((item) => stableStringify(item)).join(",")}]`; - } - if (typeof value === "object") { - const entries = Object.entries(value as Record).sort( - ([a], [b]) => a.localeCompare(b), - ); - return `{${entries - .map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`) - .join(",")}}`; - } - return JSON.stringify(String(value)); -} - -function hashString(value: string): string { - let hash = 5381; - for (let i = 0; i < value.length; i++) { - hash = (hash * 33) ^ value.charCodeAt(i); - } - return (hash >>> 0).toString(36); -} - -/** - * Create deterministic, reorder-stable React keys for DataTable rows. - * - * - Uses `identifierKey` or common identifier fields as the primary base. - * - Falls back to stable content fingerprints when no identifier exists. - * - Disambiguates duplicates without relying on array index. - */ -export function createDataTableRowKeys( - rows: Array>, - identifierKey?: string, -): string[] { - const canonicalRows = rows.map((row) => stableStringify(row)); - - const baseKeys = rows.map((row, index) => { - const identifier = getRowIdentifier( - row as Record< - string, - string | number | boolean | null | (string | number | boolean | null)[] - >, - identifierKey, - ); - - if (identifier) { - return `id:${identifier}`; - } - - return `row:${hashString(canonicalRows[index])}`; - }); - - const baseCounts = new Map(); - baseKeys.forEach((key) => { - baseCounts.set(key, (baseCounts.get(key) ?? 0) + 1); - }); - - const usedKeys = new Map(); - - return rows.map((row, index) => { - const baseKey = baseKeys[index]; - if ((baseCounts.get(baseKey) ?? 0) === 1) { - return baseKey; - } - - const rowFingerprint = hashString(canonicalRows[index]); - let disambiguatedKey = `${baseKey}::${rowFingerprint}`; - - const seenCount = usedKeys.get(disambiguatedKey) ?? 0; - usedKeys.set(disambiguatedKey, seenCount + 1); - if (seenCount > 0) { - disambiguatedKey = `${disambiguatedKey}::d${seenCount + 1}`; - } - - return disambiguatedKey; - }); -} - -function sanitizeDomIdToken(value: string): string { - return encodeURIComponent(value).replace(/%/g, "_"); -} - -export function getDataTableMobileDescriptionId(surfaceId: string): string { - return `${sanitizeDomIdToken(surfaceId)}-mobile-table-description`; -} - -/** - * Parse a string that represents a numeric value, handling various formats: - * - Currency symbols: $, €, £, ¥, etc. - * - Percent symbols: % - * - Accounting negatives: (1234) → -1234 - * - Thousands/decimal separators: 1,234.56 or 1.234,56 - * - Compact notation: 2.8T (trillion), 1.5M (million), 500K (thousand) - * - Byte suffixes: 768B (bytes), 1.5KB, 2GB, 1TB - * - * Note: Single "B" is disambiguated - integers < 1024 are bytes, otherwise billions. - * - * @param input - String to parse - * @returns Parsed number or null if unparseable - * - * @example - * parseNumericLike("$1,234.56") // 1234.56 - * parseNumericLike("2.8T") // 2800000000000 - * parseNumericLike("768B") // 768 - * parseNumericLike("50%") // 50 - * parseNumericLike("(1234)") // -1234 - */ -function parseNumericLike(input: string): number | null { - // Normalize whitespace (spaces, NBSPs, thin spaces) - let s = input.replace(/[\u00A0\u202F\s]/g, "").trim(); - if (!s) return null; - - // Accounting negatives: (1234) -> -1234 - s = s.replace(/^\((.*)\)$/g, "-$1"); - - // Strip common currency and percent symbols - s = s.replace(/[%$€£¥₩₹₽₺₪₫฿₦₴₡₲₵₸]/g, ""); - - function hasGroupedThousands(value: string, sep: "," | "."): boolean { - const unsigned = value.replace(/^[+-]/, ""); - const parts = unsigned.split(sep); - if (parts.length < 2) return false; - if (parts.some((part) => part.length === 0)) return false; - if (!/^\d{1,3}$/.test(parts[0])) return false; - if (parts[0] === "0") return false; - return parts.slice(1).every((part) => /^\d{3}$/.test(part)); - } - - const lastComma = s.lastIndexOf(","); - const lastDot = s.lastIndexOf("."); - if (lastComma !== -1 && lastDot !== -1) { - // Decide decimal by whichever occurs last - const decimalSep = lastComma > lastDot ? "," : "."; - const thousandSep = decimalSep === "," ? "." : ","; - s = s.split(thousandSep).join(""); - s = s.replace(decimalSep, "."); - } else if (lastComma !== -1) { - // Only comma present - if (hasGroupedThousands(s, ",")) { - s = s.replace(/,/g, ""); - } else { - const frac = s.length - lastComma - 1; - if (frac >= 1 && frac <= 3) s = s.replace(/,/g, "."); - else s = s.replace(/,/g, ""); - } - } else if (lastDot !== -1) { - // Only dot present; normalize grouped thousands separators. - if (hasGroupedThousands(s, ".")) { - s = s.replace(/\./g, ""); - } else if ((s.match(/\./g) || []).length > 1) { - s = s.replace(/\./g, ""); - } - } - - // Handle compact notation (K, M, B, T, P, G) and byte suffixes (KB, MB, GB, TB, PB) - const compactMatch = s.match(/^([+-]?\d+\.?\d*|\d*\.\d+)([KMBTPG]B?|B)$/i); - if (compactMatch) { - const baseNum = Number(compactMatch[1]); - if (Number.isNaN(baseNum)) return null; - - const suffix = compactMatch[2].toUpperCase(); - - // Disambiguate single "B" (bytes vs billions) - // If whole number < 1024, treat as bytes. Otherwise, billions. - if (suffix === "B") { - const isLikelyBytes = Number.isInteger(baseNum) && baseNum < 1024; - return isLikelyBytes ? baseNum : baseNum * 1e9; - } - - const multipliers: Record = { - K: 1e3, - KB: 1024, // Kilo: metric vs binary - M: 1e6, - MB: 1024 ** 2, // Mega - G: 1e9, - GB: 1024 ** 3, // Giga - T: 1e12, - TB: 1024 ** 4, // Tera - P: 1e15, - PB: 1024 ** 5, // Peta - }; - - return baseNum * (multipliers[suffix] ?? 1); - } - - if (/^[+-]?(?:\d+\.?\d*|\d*\.\d+)$/.test(s)) { - const n = Number(s); - return Number.isNaN(n) ? null : n; - } - return null; -} diff --git a/frontend/src/components/tool-ui/message-draft/README.md b/frontend/src/components/tool-ui/message-draft/README.md deleted file mode 100644 index 48ed63e2..00000000 --- a/frontend/src/components/tool-ui/message-draft/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Message Draft - -Implementation for the "message-draft" Tool UI surface. - -## Files - -- public exports: components/tool-ui/message-draft/index.tsx -- serializable schema + parse helpers: components/tool-ui/message-draft/schema.ts - -## Companion assets - -- Docs page: app/docs/message-draft/content.mdx -- Preset payload: lib/presets/message-draft.ts - -## Quick check - -Run this after edits: - -pnpm test diff --git a/frontend/src/components/tool-ui/message-draft/_adapter.tsx b/frontend/src/components/tool-ui/message-draft/_adapter.tsx deleted file mode 100644 index ef8ffcbd..00000000 --- a/frontend/src/components/tool-ui/message-draft/_adapter.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { cn } from "@/lib/utils"; -export { Button } from "@/components/ui/button"; diff --git a/frontend/src/components/tool-ui/message-draft/index.tsx b/frontend/src/components/tool-ui/message-draft/index.tsx deleted file mode 100644 index 9970ea34..00000000 --- a/frontend/src/components/tool-ui/message-draft/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export { MessageDraft } from "./message-draft"; -export { - type SerializableMessageDraft, - type SerializableEmailDraft, - type SerializableSlackDraft, - type MessageDraftChannel, - type MessageDraftOutcome, - type SlackTarget, - type MessageDraftProps, -} from "./schema"; diff --git a/frontend/src/components/tool-ui/message-draft/message-draft.tsx b/frontend/src/components/tool-ui/message-draft/message-draft.tsx deleted file mode 100644 index 1753b795..00000000 --- a/frontend/src/components/tool-ui/message-draft/message-draft.tsx +++ /dev/null @@ -1,511 +0,0 @@ -"use client"; - -import * as React from "react"; -import { cn, Button } from "./_adapter"; -import type { - MessageDraftProps, - SerializableEmailDraft, - SerializableSlackDraft, -} from "./schema"; -import { ActionButtons } from "../shared/action-buttons"; -import type { Action } from "../shared/schema"; -import { Check, ChevronDown } from "lucide-react"; - -type DraftState = "review" | "sending" | "sent" | "cancelled"; -type DraftOutcome = MessageDraftProps["outcome"]; - -const DEFAULT_GRACE_PERIOD = 5000; -const COLLAPSED_BODY_HEIGHT = 280; - -interface RecipientRowProps { - label: string; - recipients: string[]; - maxVisible?: number; - muted?: boolean; -} - -function RecipientRow({ - label, - recipients, - maxVisible = 3, - muted = false, -}: RecipientRowProps) { - const visibleRecipients = recipients.slice(0, maxVisible); - const overflowCount = recipients.length - maxVisible; - - return ( - - - {label} - - - {visibleRecipients.join(", ")} - {overflowCount > 0 && ( - +{overflowCount} more - )} - - - ); -} - -interface SingleFieldRowProps { - label: string; - value: string; -} - -function SingleFieldRow({ label, value }: SingleFieldRowProps) { - return ( - - - {label} - - {value} - - ); -} - -interface ExpandableBodyProps { - body: string; - isExpanded: boolean; - onNeedsExpansionChange?: (needsExpansion: boolean) => void; -} - -function ExpandableBody({ - body, - isExpanded, - onNeedsExpansionChange, -}: ExpandableBodyProps) { - const [needsExpansion, setNeedsExpansion] = React.useState( - null, - ); - const contentRef = React.useRef(null); - - React.useLayoutEffect(() => { - if (contentRef.current) { - const needs = contentRef.current.scrollHeight > COLLAPSED_BODY_HEIGHT; - setNeedsExpansion(needs); - onNeedsExpansionChange?.(needs); - } - }, [body, onNeedsExpansionChange]); - - return ( -
-
-

{body}

-
- {needsExpansion && ( -
- )} -
- ); -} - -interface EmailDraftContentProps { - draft: SerializableEmailDraft; - titleId: string; - isExpanded: boolean; - onNeedsExpansionChange?: (needsExpansion: boolean) => void; -} - -function EmailDraftContent({ - draft, - titleId, - isExpanded, - onNeedsExpansionChange, -}: EmailDraftContentProps) { - return ( - <> -

- {draft.subject} -

- - - - {draft.from && } - - {draft.cc && draft.cc.length > 0 && ( - - )} - {draft.bcc && draft.bcc.length > 0 && ( - - )} - -
- -
- - - - ); -} - -interface SlackDraftContentProps { - draft: SerializableSlackDraft; - titleId: string; - isExpanded: boolean; - onNeedsExpansionChange?: (needsExpansion: boolean) => void; -} - -function SlackLogo({ className }: { className?: string }) { - return ( - - ); -} - -function SlackDraftContent({ - draft, - titleId, - isExpanded, - onNeedsExpansionChange, -}: SlackDraftContentProps) { - const { target } = draft; - const isChannel = target.type === "channel"; - const targetDisplay = isChannel - ? `#${target.name}` - : `Message to @${target.name}`; - const memberCount = isChannel ? target.memberCount : undefined; - - return ( - <> -
- - {targetDisplay} - {memberCount !== undefined && ( - - {memberCount.toLocaleString()} members - - )} -
- -
- - - - ); -} - -function formatSentTime(date: Date): string { - return date.toLocaleTimeString(undefined, { - hour: "numeric", - minute: "2-digit", - }); -} - -export function resolveStateFromOutcome(outcome: DraftOutcome): DraftState { - if (outcome === "sent") return "sent"; - if (outcome === "cancelled") return "cancelled"; - return "review"; -} - -export function resolveOutcomeTransition( - previousOutcome: DraftOutcome, - nextOutcome: DraftOutcome, -): DraftState | null { - if (previousOutcome === nextOutcome) { - return null; - } - - return resolveStateFromOutcome(nextOutcome); -} - -interface SentConfirmationProps { - sentAt: Date; -} - -function SentConfirmation({ sentAt }: SentConfirmationProps) { - return ( -
- - Sent at {formatSentTime(sentAt)} - - - - -
- ); -} - -export function MessageDraft(props: MessageDraftProps) { - const { - id, - className, - outcome, - undoGracePeriod = DEFAULT_GRACE_PERIOD, - onSend, - onUndo, - onCancel, - } = props; - - const [state, setState] = React.useState(() => - resolveStateFromOutcome(outcome), - ); - const [countdown, setCountdown] = React.useState( - Math.ceil(undoGracePeriod / 1000), - ); - const [sentAt, setSentAt] = React.useState(() => - outcome === "sent" ? new Date() : null, - ); - const [isExpanded, setIsExpanded] = React.useState(false); - const [needsExpansion, setNeedsExpansion] = React.useState(false); - const undoButtonRef = React.useRef(null); - const timerRef = React.useRef | null>(null); - const countdownRef = React.useRef | null>( - null, - ); - const previousOutcomeRef = React.useRef(outcome); - - const clearTimers = React.useCallback(() => { - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - if (countdownRef.current) { - clearInterval(countdownRef.current); - countdownRef.current = null; - } - }, []); - - React.useEffect(() => { - return clearTimers; - }, [clearTimers]); - - React.useEffect(() => { - const nextState = resolveOutcomeTransition( - previousOutcomeRef.current, - outcome, - ); - - previousOutcomeRef.current = outcome; - - if (nextState === null) { - return; - } - - clearTimers(); - setState(nextState); - setCountdown(Math.ceil(undoGracePeriod / 1000)); - setSentAt(nextState === "sent" ? new Date() : null); - }, [outcome, undoGracePeriod, clearTimers]); - - React.useEffect(() => { - if (state === "sending") { - undoButtonRef.current?.focus(); - - setCountdown(Math.ceil(undoGracePeriod / 1000)); - - countdownRef.current = setInterval(() => { - setCountdown((prev) => { - if (prev <= 1) { - if (countdownRef.current) { - clearInterval(countdownRef.current); - countdownRef.current = null; - } - return 0; - } - return prev - 1; - }); - }, 1000); - - timerRef.current = setTimeout(async () => { - clearTimers(); - await onSend?.(); - setSentAt(new Date()); - setState("sent"); - }, undoGracePeriod); - } - }, [state, undoGracePeriod, onSend, clearTimers]); - - const handleSend = React.useCallback(() => { - setState("sending"); - }, []); - - const handleUndo = React.useCallback(() => { - clearTimers(); - setState("review"); - onUndo?.(); - }, [clearTimers, onUndo]); - - const handleCancel = React.useCallback(() => { - clearTimers(); - setState("cancelled"); - onCancel?.(); - }, [clearTimers, onCancel]); - - const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent) => { - if (event.key === "Escape" && state === "review") { - event.preventDefault(); - handleCancel(); - } - }, - [state, handleCancel], - ); - - const handleNeedsExpansionChange = React.useCallback((needs: boolean) => { - setNeedsExpansion(needs); - }, []); - - const handleToggleExpand = React.useCallback(() => { - setIsExpanded((prev) => !prev); - }, []); - - const handleAction = React.useCallback( - async (actionId: string) => { - if (actionId === "send") { - handleSend(); - } else if (actionId === "cancel") { - handleCancel(); - } - }, - [handleSend, handleCancel], - ); - - const actions: Action[] = [ - { - id: "cancel", - label: "Cancel", - variant: "ghost", - }, - { - id: "send", - label: "Send", - variant: "default", - }, - ]; - - const expandButton = needsExpansion ? ( - - ) : null; - - const renderActions = () => { - switch (state) { - case "sending": - return ( -
- - Sending in {countdown}s - - -
- ); - case "sent": - return ; - case "cancelled": - return null; - default: - return ; - } - }; - - if (state === "cancelled") { - return null; - } - - return ( -
-
- {props.channel === "email" ? ( - - ) : ( - - )} - - {expandButton} -
- -
{renderActions()}
-
- ); -} diff --git a/frontend/src/components/tool-ui/message-draft/schema.ts b/frontend/src/components/tool-ui/message-draft/schema.ts deleted file mode 100644 index 8cf14d5c..00000000 --- a/frontend/src/components/tool-ui/message-draft/schema.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { z } from "zod"; -import { ToolUIIdSchema, ToolUIRoleSchema } from "../shared/schema"; -import { defineToolUiContract } from "../shared/contract"; - -export const MessageDraftChannelSchema = z.enum(["email", "slack"]); - -export type MessageDraftChannel = z.infer; - -export const MessageDraftOutcomeSchema = z.enum(["sent", "cancelled"]); - -export type MessageDraftOutcome = z.infer; - -const SlackTargetSchema = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("channel"), - name: z.string().min(1), - memberCount: z.number().optional(), - }), - z.object({ type: z.literal("dm"), name: z.string().min(1) }), -]); - -export type SlackTarget = z.infer; - -export const SerializableEmailDraftSchema = z.object({ - id: ToolUIIdSchema, - role: ToolUIRoleSchema.optional(), - body: z.string().min(1), - outcome: MessageDraftOutcomeSchema.optional(), - channel: z.literal("email"), - subject: z.string().min(1), - from: z.string().optional(), - to: z.array(z.string()).min(1), - cc: z.array(z.string()).optional(), - bcc: z.array(z.string()).optional(), -}); - -export const SerializableSlackDraftSchema = z.object({ - id: ToolUIIdSchema, - role: ToolUIRoleSchema.optional(), - body: z.string().min(1), - outcome: MessageDraftOutcomeSchema.optional(), - channel: z.literal("slack"), - target: SlackTargetSchema, -}); - -export const SerializableMessageDraftSchema = z.discriminatedUnion("channel", [ - SerializableEmailDraftSchema, - SerializableSlackDraftSchema, -]); - -export type SerializableMessageDraft = z.infer< - typeof SerializableMessageDraftSchema ->; - -export type SerializableEmailDraft = z.infer< - typeof SerializableEmailDraftSchema ->; - -export type SerializableSlackDraft = z.infer< - typeof SerializableSlackDraftSchema ->; - -const SerializableMessageDraftSchemaContract = defineToolUiContract( - "MessageDraft", - SerializableMessageDraftSchema, -); - -export const parseSerializableMessageDraft: ( - input: unknown, -) => SerializableMessageDraft = SerializableMessageDraftSchemaContract.parse; - -export const safeParseSerializableMessageDraft: ( - input: unknown, -) => SerializableMessageDraft | null = - SerializableMessageDraftSchemaContract.safeParse; - -export type MessageDraftProps = SerializableMessageDraft & { - className?: string; - undoGracePeriod?: number; - onSend?: () => void | Promise; - onUndo?: () => void; - onCancel?: () => void; -}; diff --git a/frontend/src/components/tool-ui/option-list/index.tsx b/frontend/src/components/tool-ui/option-list/index.tsx deleted file mode 100644 index 7ae9d2cb..00000000 --- a/frontend/src/components/tool-ui/option-list/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export { OptionList } from "./option-list"; -export type { - OptionListProps, - OptionListOption, - OptionListSelection, - SerializableOptionList, -} from "./schema"; diff --git a/frontend/src/components/tool-ui/option-list/schema.ts b/frontend/src/components/tool-ui/option-list/schema.ts index 76a9745c..840a1fc8 100644 --- a/frontend/src/components/tool-ui/option-list/schema.ts +++ b/frontend/src/components/tool-ui/option-list/schema.ts @@ -4,13 +4,11 @@ import type { ActionsProp } from "../shared/actions-config"; import type { EmbeddedActionsProps } from "../shared/embedded-actions"; import { ActionSchema, - SerializableActionSchema, SerializableActionsConfigSchema, ToolUIIdSchema, ToolUIReceiptSchema, ToolUIRoleSchema, } from "../shared/schema"; -import { defineToolUiContract } from "../shared/contract"; export const OptionListOptionSchema = z.object({ id: z.string().min(1), @@ -173,38 +171,4 @@ export type OptionListProps = Omit< onAction?: EmbeddedActionsProps["onAction"]; onBeforeAction?: EmbeddedActionsProps["onBeforeAction"]; className?: string; -}; - -export const SerializableOptionListSchema = OptionListPropsSchemaBase.omit({ - // Exclude controlled selection from tool/LLM payloads. - value: true, -}) - .extend({ - options: z.array(OptionListOptionSchema.omit({ icon: true })), - actions: z - .union([ - z.array(SerializableActionSchema), - SerializableActionsConfigSchema, - ]) - .optional(), - }) - .strict() - .superRefine(validateOptionListInvariants); - -export type SerializableOptionList = z.infer< - typeof SerializableOptionListSchema ->; - -const SerializableOptionListSchemaContract = defineToolUiContract( - "OptionList", - SerializableOptionListSchema, -); - -export const parseSerializableOptionList: ( - input: unknown, -) => SerializableOptionList = SerializableOptionListSchemaContract.parse; - -export const safeParseSerializableOptionList: ( - input: unknown, -) => SerializableOptionList | null = - SerializableOptionListSchemaContract.safeParse; +}; \ No newline at end of file diff --git a/frontend/src/components/tool-ui/progress-tracker/README.md b/frontend/src/components/tool-ui/progress-tracker/README.md deleted file mode 100644 index 99adb245..00000000 --- a/frontend/src/components/tool-ui/progress-tracker/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Progress Tracker - -Implementation for the "progress-tracker" Tool UI surface. - -## Files - -- public exports: components/tool-ui/progress-tracker/index.tsx -- serializable schema + parse helpers: components/tool-ui/progress-tracker/schema.ts - -## Companion assets - -- Docs page: app/docs/progress-tracker/content.mdx -- Preset payload: lib/presets/progress-tracker.ts - -## Quick check - -Run this after edits: - -pnpm test diff --git a/frontend/src/components/tool-ui/progress-tracker/_adapter.tsx b/frontend/src/components/tool-ui/progress-tracker/_adapter.tsx deleted file mode 100644 index 9fa42b2b..00000000 --- a/frontend/src/components/tool-ui/progress-tracker/_adapter.tsx +++ /dev/null @@ -1 +0,0 @@ -export { cn } from "@/lib/utils"; diff --git a/frontend/src/components/tool-ui/progress-tracker/index.tsx b/frontend/src/components/tool-ui/progress-tracker/index.tsx deleted file mode 100644 index 9aebea9b..00000000 --- a/frontend/src/components/tool-ui/progress-tracker/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export { ProgressTracker } from "./progress-tracker"; -export { - type SerializableProgressTracker, - type ProgressTrackerProps, - type ProgressTrackerChoice, - type ProgressStep, -} from "./schema"; diff --git a/frontend/src/components/tool-ui/progress-tracker/progress-tracker.tsx b/frontend/src/components/tool-ui/progress-tracker/progress-tracker.tsx deleted file mode 100644 index 3d130aca..00000000 --- a/frontend/src/components/tool-ui/progress-tracker/progress-tracker.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import { cn } from "./_adapter"; -import type { - ProgressStep, - ProgressTrackerChoice, - ProgressTrackerProps, -} from "./schema"; -import { Check, X, Loader2, Timer, AlertCircle } from "lucide-react"; -import type { LucideIcon } from "lucide-react"; - -function formatElapsedTime(milliseconds: number): string { - const roundedSeconds = Math.round(Math.max(0, milliseconds) / 100) / 10; - - if (roundedSeconds < 60) { - return `${roundedSeconds.toFixed(1)}s`; - } - - const wholeSeconds = Math.floor(roundedSeconds); - const minutes = Math.floor(wholeSeconds / 60); - const remainingSeconds = wholeSeconds % 60; - return `${minutes}m ${remainingSeconds}s`; -} - -function formatElapsedTimeDateTime(milliseconds: number): string { - const roundedSeconds = Math.round(Math.max(0, milliseconds) / 100) / 10; - - if (roundedSeconds < 60) { - return `PT${Number(roundedSeconds.toFixed(1))}S`; - } - - const wholeSeconds = Math.floor(roundedSeconds); - const hours = Math.floor(wholeSeconds / 3600); - const minutes = Math.floor((wholeSeconds % 3600) / 60); - const seconds = wholeSeconds % 60; - - const hourPart = hours > 0 ? `${hours}H` : ""; - const minutePart = minutes > 0 ? `${minutes}M` : ""; - const secondPart = seconds > 0 ? `${seconds}S` : ""; - - if (!hourPart && !minutePart && !secondPart) { - return "PT0S"; - } - - return `PT${hourPart}${minutePart}${secondPart}`; -} - -function getCurrentStepId(steps: ProgressStep[]): string | null { - const inProgressStep = steps.find((s) => s.status === "in-progress"); - if (inProgressStep) return inProgressStep.id; - - const failedStep = steps.find((s) => s.status === "failed"); - if (failedStep) return failedStep.id; - - const firstPendingStep = steps.find((s) => s.status === "pending"); - if (firstPendingStep) return firstPendingStep.id; - - return null; -} - -function getReceiptState(outcome: ProgressTrackerChoice["outcome"]): { - toneClassName: string; - icon: LucideIcon; -} { - switch (outcome) { - case "success": - return { - toneClassName: "text-emerald-600 dark:text-emerald-500", - icon: Check, - }; - case "partial": - return { - toneClassName: "text-amber-600 dark:text-amber-500", - icon: AlertCircle, - }; - case "failed": - return { - toneClassName: "text-destructive", - icon: AlertCircle, - }; - case "cancelled": - return { - toneClassName: "text-muted-foreground", - icon: X, - }; - } -} - -interface StepIndicatorProps { - status: "pending" | "in-progress" | "completed" | "failed"; -} - -function StepIndicator({ status }: StepIndicatorProps) { - if (status === "pending") { - return ( -