Skip to content

Commit dd601ea

Browse files
committed
feat: terminal serach; fix: delete-modal
1 parent 3158b62 commit dd601ea

File tree

5 files changed

+399
-85
lines changed

5 files changed

+399
-85
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-output-panel-resize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useTerminalStore } from '@/stores/terminal'
55
* Constants for output panel sizing
66
* Must match MIN_OUTPUT_PANEL_WIDTH_PX and BLOCK_COLUMN_WIDTH_PX in terminal.tsx
77
*/
8-
const MIN_WIDTH = 300
8+
const MIN_WIDTH = 440
99
const BLOCK_COLUMN_WIDTH = 240
1010

1111
/**

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx

Lines changed: 216 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ import {
1313
FilterX,
1414
MoreHorizontal,
1515
RepeatIcon,
16+
Search,
1617
SplitIcon,
1718
Trash2,
19+
X,
1820
} from 'lucide-react'
1921
import {
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
*/
5154
const 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

Comments
 (0)