@@ -13,12 +13,15 @@ import {
1313 FilterX ,
1414 MoreHorizontal ,
1515 RepeatIcon ,
16+ Search ,
1617 SplitIcon ,
1718 Trash2 ,
19+ X ,
1820} from 'lucide-react'
1921import {
2022 Button ,
2123 Code ,
24+ Input ,
2225 Popover ,
2326 PopoverContent ,
2427 PopoverItem ,
@@ -49,7 +52,7 @@ const DEFAULT_EXPANDED_HEIGHT = 196
4952 * Column width constants - numeric values for calculations
5053 */
5154const BLOCK_COLUMN_WIDTH_PX = 240
52- const MIN_OUTPUT_PANEL_WIDTH_PX = 300
55+ const MIN_OUTPUT_PANEL_WIDTH_PX = 440
5356
5457/**
5558 * Column width constants - Tailwind classes for styling
@@ -154,7 +157,7 @@ const ToggleButton = ({
154157 isExpanded : boolean
155158 onClick : ( e : React . MouseEvent ) => void
156159} ) => (
157- < Button variant = 'ghost' className = '!p-0 ' onClick = { onClick } aria-label = 'Toggle terminal' >
160+ < Button variant = 'ghost' className = '!p-1.5 -m-1.5 ' onClick = { onClick } aria-label = 'Toggle terminal' >
158161 < ChevronDown
159162 className = { clsx (
160163 'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100' ,
@@ -258,8 +261,6 @@ export function Terminal() {
258261 setOutputPanelWidth,
259262 openOnRun,
260263 setOpenOnRun,
261- // displayMode,
262- // setDisplayMode,
263264 setHasHydrated,
264265 } = useTerminalStore ( )
265266 const entries = useTerminalConsoleStore ( ( state ) => state . entries )
@@ -268,7 +269,6 @@ export function Terminal() {
268269 const { activeWorkflowId } = useWorkflowRegistry ( )
269270 const [ selectedEntry , setSelectedEntry ] = useState < ConsoleEntry | null > ( null )
270271 const [ isToggling , setIsToggling ] = useState ( false )
271- // const [displayPopoverOpen, setDisplayPopoverOpen] = useState(false)
272272 const [ wrapText , setWrapText ] = useState ( true )
273273 const [ showCopySuccess , setShowCopySuccess ] = useState ( false )
274274 const [ showInput , setShowInput ] = useState ( false )
@@ -279,6 +279,14 @@ export function Terminal() {
279279 const [ mainOptionsOpen , setMainOptionsOpen ] = useState ( false )
280280 const [ outputOptionsOpen , setOutputOptionsOpen ] = useState ( false )
281281
282+ // Output panel search state
283+ const [ isOutputSearchActive , setIsOutputSearchActive ] = useState ( false )
284+ const [ outputSearchQuery , setOutputSearchQuery ] = useState ( '' )
285+ const [ matchCount , setMatchCount ] = useState ( 0 )
286+ const [ currentMatchIndex , setCurrentMatchIndex ] = useState ( 0 )
287+ const outputSearchInputRef = useRef < HTMLInputElement > ( null )
288+ const outputContentRef = useRef < HTMLDivElement > ( null )
289+
282290 // Terminal resize hooks
283291 const { handleMouseDown } = useTerminalResize ( )
284292 const { handleMouseDown : handleOutputPanelResizeMouseDown } = useOutputPanelResize ( )
@@ -497,6 +505,50 @@ export function Terminal() {
497505 }
498506 } , [ activeWorkflowId , clearWorkflowConsole ] )
499507
508+ /**
509+ * Activates output search and focuses the search input.
510+ */
511+ const activateOutputSearch = useCallback ( ( ) => {
512+ setIsOutputSearchActive ( true )
513+ setTimeout ( ( ) => {
514+ outputSearchInputRef . current ?. focus ( )
515+ } , 0 )
516+ } , [ ] )
517+
518+ /**
519+ * Closes output search and clears the query.
520+ */
521+ const closeOutputSearch = useCallback ( ( ) => {
522+ setIsOutputSearchActive ( false )
523+ setOutputSearchQuery ( '' )
524+ setMatchCount ( 0 )
525+ setCurrentMatchIndex ( 0 )
526+ } , [ ] )
527+
528+ /**
529+ * Navigates to the next match in the search results.
530+ */
531+ const goToNextMatch = useCallback ( ( ) => {
532+ if ( matchCount === 0 ) return
533+ setCurrentMatchIndex ( ( prev ) => ( prev + 1 ) % matchCount )
534+ } , [ matchCount ] )
535+
536+ /**
537+ * Navigates to the previous match in the search results.
538+ */
539+ const goToPreviousMatch = useCallback ( ( ) => {
540+ if ( matchCount === 0 ) return
541+ setCurrentMatchIndex ( ( prev ) => ( prev - 1 + matchCount ) % matchCount )
542+ } , [ matchCount ] )
543+
544+ /**
545+ * Handles match count change from Code.Viewer.
546+ */
547+ const handleMatchCountChange = useCallback ( ( count : number ) => {
548+ setMatchCount ( count )
549+ setCurrentMatchIndex ( 0 )
550+ } , [ ] )
551+
500552 /**
501553 * Handle clear console for current workflow via mouse interaction.
502554 */
@@ -681,20 +733,66 @@ export function Terminal() {
681733 } , [ expandToLastHeight , selectedEntry , showInput , hasInputData , isExpanded ] )
682734
683735 /**
684- * Handle Escape to unselect and Enter to re-enable auto-selection
736+ * Handle Escape to close search or unselect entry
685737 */
686738 useEffect ( ( ) => {
687739 const handleKeyDown = ( e : KeyboardEvent ) => {
688- if ( e . key === 'Escape' && selectedEntry ) {
740+ if ( e . key === 'Escape' ) {
689741 e . preventDefault ( )
690- setSelectedEntry ( null )
691- setAutoSelectEnabled ( true )
742+ // First close search if active
743+ if ( isOutputSearchActive ) {
744+ closeOutputSearch ( )
745+ return
746+ }
747+ // Then unselect entry
748+ if ( selectedEntry ) {
749+ setSelectedEntry ( null )
750+ setAutoSelectEnabled ( true )
751+ }
692752 }
693753 }
694754
695755 window . addEventListener ( 'keydown' , handleKeyDown )
696756 return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown )
697- } , [ selectedEntry ] )
757+ } , [ selectedEntry , isOutputSearchActive , closeOutputSearch ] )
758+
759+ /**
760+ * Handle Enter/Shift+Enter for search navigation when search input is focused
761+ */
762+ useEffect ( ( ) => {
763+ const handleKeyDown = ( e : KeyboardEvent ) => {
764+ if ( ! isOutputSearchActive ) return
765+
766+ const isSearchInputFocused = document . activeElement === outputSearchInputRef . current
767+
768+ if ( e . key === 'Enter' && isSearchInputFocused && matchCount > 0 ) {
769+ e . preventDefault ( )
770+ if ( e . shiftKey ) {
771+ goToPreviousMatch ( )
772+ } else {
773+ goToNextMatch ( )
774+ }
775+ }
776+ }
777+
778+ window . addEventListener ( 'keydown' , handleKeyDown )
779+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown )
780+ } , [ isOutputSearchActive , matchCount , goToNextMatch , goToPreviousMatch ] )
781+
782+ /**
783+ * Scroll to current match when it changes
784+ */
785+ useEffect ( ( ) => {
786+ if ( ! isOutputSearchActive || matchCount === 0 || ! outputContentRef . current ) return
787+
788+ // Find all match elements and scroll to the current one
789+ const matchElements = outputContentRef . current . querySelectorAll ( '[data-search-match]' )
790+ const currentElement = matchElements [ currentMatchIndex ]
791+
792+ if ( currentElement ) {
793+ currentElement . scrollIntoView ( { block : 'center' } )
794+ }
795+ } , [ currentMatchIndex , isOutputSearchActive , matchCount ] )
698796
699797 /**
700798 * Adjust output panel width when sidebar or panel width changes.
@@ -1206,7 +1304,7 @@ export function Terminal() {
12061304
12071305 { /* Header */ }
12081306 < div
1209- className = 'group flex h-[30px] flex-shrink-0 cursor-pointer items-center justify-between bg-[var(--surface-1)] px -[16px]'
1307+ className = 'group flex h-[30px] flex-shrink-0 cursor-pointer items-center justify-between bg-[var(--surface-1)] pr -[16px] pl-[10px ]'
12101308 onClick = { handleHeaderClick }
12111309 >
12121310 < div className = 'flex items-center' >
@@ -1234,7 +1332,7 @@ export function Terminal() {
12341332 variant = 'ghost'
12351333 className = { clsx (
12361334 'px-[8px] py-[6px] text-[12px]' ,
1237- showInput && '!text-[var(--text-primary)] dark:!text-[var(--text-primary)] '
1335+ showInput && '!text-[var(--text-primary)]'
12381336 ) }
12391337 onClick = { ( e ) => {
12401338 e . stopPropagation ( )
@@ -1249,7 +1347,47 @@ export function Terminal() {
12491347 </ Button >
12501348 ) }
12511349 </ div >
1252- < div className = 'flex items-center gap-[8px]' >
1350+ < div className = 'flex flex-shrink-0 items-center gap-[8px]' >
1351+ { isOutputSearchActive ? (
1352+ < Tooltip . Root >
1353+ < Tooltip . Trigger asChild >
1354+ < Button
1355+ variant = 'ghost'
1356+ onClick = { ( e ) => {
1357+ e . stopPropagation ( )
1358+ closeOutputSearch ( )
1359+ } }
1360+ aria-label = 'Search in output'
1361+ className = '!p-1.5 -m-1.5'
1362+ >
1363+ < X className = 'h-[12px] w-[12px]' />
1364+ </ Button >
1365+ </ Tooltip . Trigger >
1366+ < Tooltip . Content >
1367+ < span > Close search</ span >
1368+ </ Tooltip . Content >
1369+ </ Tooltip . Root >
1370+ ) : (
1371+ < Tooltip . Root >
1372+ < Tooltip . Trigger asChild >
1373+ < Button
1374+ variant = 'ghost'
1375+ onClick = { ( e ) => {
1376+ e . stopPropagation ( )
1377+ activateOutputSearch ( )
1378+ } }
1379+ aria-label = 'Search in output'
1380+ className = '!p-1.5 -m-1.5'
1381+ >
1382+ < Search className = 'h-[12px] w-[12px]' />
1383+ </ Button >
1384+ </ Tooltip . Trigger >
1385+ < Tooltip . Content >
1386+ < span > Search</ span >
1387+ </ Tooltip . Content >
1388+ </ Tooltip . Root >
1389+ ) }
1390+
12531391 < Tooltip . Root >
12541392 < Tooltip . Trigger asChild >
12551393 < Button
@@ -1262,7 +1400,7 @@ export function Terminal() {
12621400 className = '!p-1.5 -m-1.5'
12631401 >
12641402 { showCopySuccess ? (
1265- < Check className = 'h-3.5 w-3.5 ' />
1403+ < Check className = 'h-[12px] w-[12px] ' />
12661404 ) : (
12671405 < Clipboard className = 'h-[12px] w-[12px]' />
12681406 ) }
@@ -1380,14 +1518,63 @@ export function Terminal() {
13801518 </ div >
13811519 </ div >
13821520
1521+ { /* Search Overlay */ }
1522+ { isOutputSearchActive && (
1523+ < div
1524+ className = 'absolute top-[30px] right-[8px] z-30 flex h-[34px] items-center gap-[6px] rounded-b-[4px] border border-[var(--border)] border-t-0 bg-[var(--surface-1)] px-[6px] shadow-sm'
1525+ onClick = { ( e ) => e . stopPropagation ( ) }
1526+ data-toolbar-root
1527+ data-search-active = 'true'
1528+ >
1529+ < Input
1530+ ref = { outputSearchInputRef }
1531+ type = 'text'
1532+ value = { outputSearchQuery }
1533+ onChange = { ( e ) => setOutputSearchQuery ( e . target . value ) }
1534+ placeholder = 'Search...'
1535+ className = 'mr-[2px] h-[23px] w-[94px] text-[12px]'
1536+ />
1537+ < span
1538+ className = { clsx (
1539+ 'w-[58px] font-medium text-[11px]' ,
1540+ matchCount > 0
1541+ ? 'text-[var(--text-secondary)]'
1542+ : 'text-[var(--text-tertiary)]'
1543+ ) }
1544+ >
1545+ { matchCount > 0 ? `${ currentMatchIndex + 1 } /${ matchCount } ` : 'No results' }
1546+ </ span >
1547+ < Button
1548+ variant = 'ghost'
1549+ onClick = { goToPreviousMatch }
1550+ aria-label = 'Previous match'
1551+ className = '!p-1.5 -m-1.5'
1552+ disabled = { matchCount === 0 }
1553+ >
1554+ < ArrowUp className = 'h-[12px] w-[12px]' />
1555+ </ Button >
1556+ < Button
1557+ variant = 'ghost'
1558+ onClick = { goToNextMatch }
1559+ aria-label = 'Next match'
1560+ className = '!p-1.5 -m-1.5'
1561+ disabled = { matchCount === 0 }
1562+ >
1563+ < ArrowDown className = 'h-[12px] w-[12px]' />
1564+ </ Button >
1565+ < Button
1566+ variant = 'ghost'
1567+ onClick = { closeOutputSearch }
1568+ aria-label = 'Close search'
1569+ className = '!p-1.5 -m-1.5'
1570+ >
1571+ < X className = 'h-[12px] w-[12px]' />
1572+ </ Button >
1573+ </ div >
1574+ ) }
1575+
13831576 { /* Content */ }
1384- < div
1385- className = 'flex-1 overflow-x-auto overflow-y-auto'
1386- // className={clsx(
1387- // 'flex-1 overflow-x-auto overflow-y-auto',
1388- // displayMode === 'prettier' && 'px-[8px] pb-[8px]'
1389- // )}
1390- >
1577+ < div className = 'flex-1 overflow-x-auto overflow-y-auto' >
13911578 { shouldShowCodeDisplay ? (
13921579 < Code . Viewer
13931580 code = { selectedEntry . input . code }
@@ -1399,6 +1586,10 @@ export function Terminal() {
13991586 paddingLeft = { 8 }
14001587 gutterStyle = { { backgroundColor : 'transparent' } }
14011588 wrapText = { wrapText }
1589+ searchQuery = { isOutputSearchActive ? outputSearchQuery : undefined }
1590+ currentMatchIndex = { currentMatchIndex }
1591+ onMatchCountChange = { handleMatchCountChange }
1592+ contentRef = { outputContentRef }
14021593 />
14031594 ) : (
14041595 < Code . Viewer
@@ -1409,21 +1600,12 @@ export function Terminal() {
14091600 paddingLeft = { 8 }
14101601 gutterStyle = { { backgroundColor : 'transparent' } }
14111602 wrapText = { wrapText }
1603+ searchQuery = { isOutputSearchActive ? outputSearchQuery : undefined }
1604+ currentMatchIndex = { currentMatchIndex }
1605+ onMatchCountChange = { handleMatchCountChange }
1606+ contentRef = { outputContentRef }
14121607 />
14131608 ) }
1414- { /* ) : displayMode === 'raw' ? (
1415- <Code.Viewer
1416- code={JSON.stringify(outputData, null, 2)}
1417- showGutter
1418- language='json'
1419- className='m-0 min-h-full rounded-none border-0 bg-[var(--surface-1)]'
1420- paddingLeft={8}
1421- gutterStyle={{ backgroundColor: 'transparent' }}
1422- wrapText={wrapText}
1423- />
1424- ) : (
1425- <PrettierOutput output={outputData} wrapText={wrapText} />
1426- )} */ }
14271609 </ div >
14281610 </ div >
14291611 ) }
0 commit comments