diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts index bdafb76c6..e03c80c57 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts @@ -5,7 +5,7 @@ import { useTerminalStore } from '@/stores/terminal' * Constants for output panel sizing * Must match MIN_OUTPUT_PANEL_WIDTH_PX and BLOCK_COLUMN_WIDTH_PX in terminal.tsx */ -const MIN_WIDTH = 300 +const MIN_WIDTH = 440 const BLOCK_COLUMN_WIDTH = 240 /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 7949d3d7b..e3c2942ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -13,12 +13,15 @@ import { FilterX, MoreHorizontal, RepeatIcon, + Search, SplitIcon, Trash2, + X, } from 'lucide-react' import { Button, Code, + Input, Popover, PopoverContent, PopoverItem, @@ -49,7 +52,7 @@ const DEFAULT_EXPANDED_HEIGHT = 196 * Column width constants - numeric values for calculations */ const BLOCK_COLUMN_WIDTH_PX = 240 -const MIN_OUTPUT_PANEL_WIDTH_PX = 300 +const MIN_OUTPUT_PANEL_WIDTH_PX = 440 /** * Column width constants - Tailwind classes for styling @@ -154,7 +157,7 @@ const ToggleButton = ({ isExpanded: boolean onClick: (e: React.MouseEvent) => void }) => ( - )} -
+
+ {isOutputSearchActive ? ( + + + + + + Close search + + + ) : ( + + + + + + Search + + + )} +
+ {/* Search Overlay */} + {isOutputSearchActive && ( +
e.stopPropagation()} + data-toolbar-root + data-search-active='true' + > + setOutputSearchQuery(e.target.value)} + placeholder='Search...' + className='mr-[2px] h-[23px] w-[94px] text-[12px]' + /> + 0 + ? 'text-[var(--text-secondary)]' + : 'text-[var(--text-tertiary)]' + )} + > + {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : 'No results'} + + + + +
+ )} + {/* Content */} -
+
{shouldShowCodeDisplay ? ( ) : ( )} - {/* ) : displayMode === 'raw' ? ( - - ) : ( - - )} */}
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/workflow-list/components/delete-modal/delete-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/workflow-list/components/delete-modal/delete-modal.tsx index ee61282ff..ce870bc83 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/workflow-list/components/delete-modal/delete-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/workflow-list/components/delete-modal/delete-modal.tsx @@ -1,14 +1,13 @@ 'use client' +import { Button } from '@/components/emcn' import { Modal, + ModalBody, ModalContent, - ModalDescription, ModalFooter, ModalHeader, - ModalTitle, -} from '@/components/emcn' -import { Button } from '@/components/ui/button' +} from '@/components/emcn/components/modal/modal' interface DeleteModalProps { /** @@ -60,59 +59,73 @@ export function DeleteModal({ let title = '' if (itemType === 'workflow') { - title = isMultiple ? 'Delete workflows?' : 'Delete workflow?' + title = isMultiple ? 'Delete Workflows' : 'Delete Workflow' } else if (itemType === 'folder') { - title = 'Delete folder?' + title = 'Delete Folder' } else { - title = 'Delete workspace?' + title = 'Delete Workspace' } - let description = '' - if (itemType === 'workflow') { - if (isMultiple) { - const workflowList = displayNames.join(', ') - description = `Deleting ${workflowList} will permanently remove all associated blocks, executions, and configuration.` - } else if (isSingle && displayNames.length > 0) { - description = `Deleting ${displayNames[0]} will permanently remove all associated blocks, executions, and configuration.` - } else { - description = - 'Deleting this workflow will permanently remove all associated blocks, executions, and configuration.' + const renderDescription = () => { + if (itemType === 'workflow') { + if (isMultiple) { + return ( + <> + Are you sure you want to delete{' '} + + {displayNames.join(', ')} + + ? This will permanently remove all associated blocks, executions, and configuration. + + ) + } + if (isSingle && displayNames.length > 0) { + return ( + <> + Are you sure you want to delete{' '} + {displayNames[0]}? This + will permanently remove all associated blocks, executions, and configuration. + + ) + } + return 'Are you sure you want to delete this workflow? This will permanently remove all associated blocks, executions, and configuration.' } - } else if (itemType === 'folder') { - if (isSingle && displayNames.length > 0) { - description = `Deleting ${displayNames[0]} will permanently remove all associated workflows, logs, and knowledge bases.` - } else { - description = - 'Deleting this folder will permanently remove all associated workflows, logs, and knowledge bases.' + + if (itemType === 'folder') { + if (isSingle && displayNames.length > 0) { + return ( + <> + Are you sure you want to delete{' '} + {displayNames[0]}? This + will permanently remove all associated workflows, logs, and knowledge bases. + + ) + } + return 'Are you sure you want to delete this folder? This will permanently remove all associated workflows, logs, and knowledge bases.' } - } else { - description = - 'Deleting this workspace will permanently remove all associated workflows, folders, logs, and knowledge bases.' + + return 'Are you sure you want to delete this workspace? This will permanently remove all associated workflows, folders, logs, and knowledge bases.' } return ( - - - {title} - - {description}{' '} + + {title} + +

+ {renderDescription()}{' '} This action cannot be undone. - - +

+
- diff --git a/apps/sim/components/emcn/components/code/code.tsx b/apps/sim/components/emcn/components/code/code.tsx index bba8e683d..d4390ff8a 100644 --- a/apps/sim/components/emcn/components/code/code.tsx +++ b/apps/sim/components/emcn/components/code/code.tsx @@ -1,4 +1,4 @@ -import { Fragment, type ReactNode } from 'react' +import { Fragment, type ReactNode, useEffect, useMemo } from 'react' import { highlight, languages } from 'prismjs' import 'prismjs/components/prism-javascript' import 'prismjs/components/prism-python' @@ -272,11 +272,91 @@ interface CodeViewerProps { gutterStyle?: React.CSSProperties /** Whether to wrap text instead of using horizontal scroll */ wrapText?: boolean + /** Search query to highlight in the code */ + searchQuery?: string + /** Index of the currently active match (for distinct highlighting) */ + currentMatchIndex?: number + /** Callback when match count changes */ + onMatchCountChange?: (count: number) => void + /** Ref for the content container (for scrolling to matches) */ + contentRef?: React.RefObject +} + +/** + * Escapes special regex characters in a string. + */ +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +/** + * Applies search highlighting to already syntax-highlighted HTML. + * Wraps matches in spans with appropriate highlighting classes. + * + * @param html - The syntax-highlighted HTML string + * @param searchQuery - The search query to highlight + * @param currentMatchIndex - Index of the current match (for distinct highlighting) + * @param matchCounter - Mutable counter object to track match indices across calls + * @returns The HTML with search highlighting applied + */ +function applySearchHighlighting( + html: string, + searchQuery: string, + currentMatchIndex: number, + matchCounter: { count: number } +): string { + if (!searchQuery.trim()) return html + + const escaped = escapeRegex(searchQuery) + const regex = new RegExp(`(${escaped})`, 'gi') + + // We need to be careful not to match inside HTML tags + // Split by HTML tags and only process text parts + const parts = html.split(/(<[^>]+>)/g) + + return parts + .map((part) => { + // If it's an HTML tag, don't modify it + if (part.startsWith('<') && part.endsWith('>')) { + return part + } + + // Process text content + return part.replace(regex, (match) => { + const isCurrentMatch = matchCounter.count === currentMatchIndex + matchCounter.count++ + + const bgClass = isCurrentMatch + ? 'bg-[#F6AD55] text-[#1a1a1a] dark:bg-[#F6AD55] dark:text-[#1a1a1a]' + : 'bg-[#FCD34D]/40 dark:bg-[#FCD34D]/30' + + return `${match}` + }) + }) + .join('') +} + +/** + * Counts all matches for a search query in the given code. + * + * @param code - The raw code string + * @param searchQuery - The search query + * @returns Number of matches found + */ +function countSearchMatches(code: string, searchQuery: string): number { + if (!searchQuery.trim()) return 0 + + const escaped = escapeRegex(searchQuery) + const regex = new RegExp(escaped, 'gi') + const matches = code.match(regex) + + return matches?.length ?? 0 } /** * Readonly code viewer with optional gutter and syntax highlighting. * Handles all complexity internally - line numbers, gutter width calculation, and highlighting. + * Supports optional search highlighting with navigation. * * @example * ```tsx @@ -284,6 +364,8 @@ interface CodeViewerProps { * code={JSON.stringify(data, null, 2)} * showGutter * language="json" + * searchQuery="error" + * currentMatchIndex={0} * /> * ``` */ @@ -295,9 +377,17 @@ function Viewer({ paddingLeft = 0, gutterStyle, wrapText = false, + searchQuery, + currentMatchIndex = 0, + onMatchCountChange, + contentRef, }: CodeViewerProps) { - // Apply syntax highlighting using the specified language - const highlightedCode = highlight(code, languages[language] || languages.javascript, language) + // Compute match count and notify parent + const matchCount = useMemo(() => countSearchMatches(code, searchQuery || ''), [code, searchQuery]) + + useEffect(() => { + onMatchCountChange?.(matchCount) + }, [matchCount, onMatchCountChange]) // Determine whitespace class based on wrap setting const whitespaceClass = wrapText ? 'whitespace-pre-wrap break-words' : 'whitespace-pre' @@ -307,10 +397,11 @@ function Viewer({ if (showGutter && wrapText) { const lines = code.split('\n') const gutterWidth = calculateGutterWidth(lines.length) + const matchCounter = { count: 0 } return ( - +
{lines.map((line, idx) => { - const perLineHighlighted = highlight( + let perLineHighlighted = highlight( line, languages[language] || languages.javascript, language ) + + // Apply search highlighting if query exists + if (searchQuery?.trim()) { + perLineHighlighted = applySearchHighlighting( + perLineHighlighted, + searchQuery, + currentMatchIndex, + matchCounter + ) + } + return (
                 
@@ -348,11 +449,25 @@ function Viewer({
     )
   }
 
+  // Apply syntax highlighting
+  let highlightedCode = highlight(code, languages[language] || languages.javascript, language)
+
+  // Apply search highlighting if query exists
+  if (searchQuery?.trim()) {
+    const matchCounter = { count: 0 }
+    highlightedCode = applySearchHighlighting(
+      highlightedCode,
+      searchQuery,
+      currentMatchIndex,
+      matchCounter
+    )
+  }
+
   if (!showGutter) {
     // Simple display without gutter
     return (
       
-        
+        
           
         {lineNumbers}
       
-      
+