mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(terminal): added terminal context menu (#2692)
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
|
||||
interface ChunkContextMenuProps {
|
||||
isOpen: boolean
|
||||
@@ -102,6 +108,7 @@ export function ChunkContextMenu({
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
{hasChunk ? (
|
||||
<>
|
||||
{/* Navigation */}
|
||||
{!isMultiSelect && onOpenInNewTab && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
@@ -112,6 +119,9 @@ export function ChunkContextMenu({
|
||||
Open in new tab
|
||||
</PopoverItem>
|
||||
)}
|
||||
{!isMultiSelect && onOpenInNewTab && <PopoverDivider />}
|
||||
|
||||
{/* Edit and copy actions */}
|
||||
{!isMultiSelect && onEdit && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
@@ -132,6 +142,9 @@ export function ChunkContextMenu({
|
||||
Copy content
|
||||
</PopoverItem>
|
||||
)}
|
||||
{!isMultiSelect && (onEdit || onCopyContent) && <PopoverDivider />}
|
||||
|
||||
{/* State toggle */}
|
||||
{onToggleEnabled && (
|
||||
<PopoverItem
|
||||
disabled={disableToggleEnabled}
|
||||
@@ -143,6 +156,9 @@ export function ChunkContextMenu({
|
||||
{getToggleLabel()}
|
||||
</PopoverItem>
|
||||
)}
|
||||
|
||||
{/* Destructive action */}
|
||||
{onToggleEnabled && onDelete && <PopoverDivider />}
|
||||
{onDelete && (
|
||||
<PopoverItem
|
||||
disabled={disableDelete}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
|
||||
interface DocumentContextMenuProps {
|
||||
isOpen: boolean
|
||||
@@ -107,6 +113,7 @@ export function DocumentContextMenu({
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
{hasDocument ? (
|
||||
<>
|
||||
{/* Navigation */}
|
||||
{!isMultiSelect && onOpenInNewTab && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
@@ -117,6 +124,9 @@ export function DocumentContextMenu({
|
||||
Open in new tab
|
||||
</PopoverItem>
|
||||
)}
|
||||
{!isMultiSelect && onOpenInNewTab && <PopoverDivider />}
|
||||
|
||||
{/* Edit and view actions */}
|
||||
{!isMultiSelect && onRename && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
@@ -137,6 +147,9 @@ export function DocumentContextMenu({
|
||||
View tags
|
||||
</PopoverItem>
|
||||
)}
|
||||
{!isMultiSelect && (onRename || (hasTags && onViewTags)) && <PopoverDivider />}
|
||||
|
||||
{/* State toggle */}
|
||||
{onToggleEnabled && (
|
||||
<PopoverItem
|
||||
disabled={disableToggleEnabled}
|
||||
@@ -148,6 +161,9 @@ export function DocumentContextMenu({
|
||||
{getToggleLabel()}
|
||||
</PopoverItem>
|
||||
)}
|
||||
|
||||
{/* Destructive action */}
|
||||
{onToggleEnabled && onDelete && <PopoverDivider />}
|
||||
{onDelete && (
|
||||
<PopoverItem
|
||||
disabled={disableDelete}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
|
||||
interface KnowledgeBaseContextMenuProps {
|
||||
/**
|
||||
@@ -104,6 +110,7 @@ export function KnowledgeBaseContextMenu({
|
||||
}}
|
||||
/>
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
{/* Navigation */}
|
||||
{showOpenInNewTab && onOpenInNewTab && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
@@ -114,6 +121,9 @@ export function KnowledgeBaseContextMenu({
|
||||
Open in new tab
|
||||
</PopoverItem>
|
||||
)}
|
||||
{showOpenInNewTab && onOpenInNewTab && <PopoverDivider />}
|
||||
|
||||
{/* View and copy actions */}
|
||||
{showViewTags && onViewTags && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
@@ -134,6 +144,9 @@ export function KnowledgeBaseContextMenu({
|
||||
Copy ID
|
||||
</PopoverItem>
|
||||
)}
|
||||
{((showViewTags && onViewTags) || onCopyId) && <PopoverDivider />}
|
||||
|
||||
{/* Edit action */}
|
||||
{showEdit && onEdit && (
|
||||
<PopoverItem
|
||||
disabled={disableEdit}
|
||||
@@ -145,6 +158,9 @@ export function KnowledgeBaseContextMenu({
|
||||
Edit
|
||||
</PopoverItem>
|
||||
)}
|
||||
|
||||
{/* Destructive action */}
|
||||
{showEdit && onEdit && showDelete && onDelete && <PopoverDivider />}
|
||||
{showDelete && onDelete && (
|
||||
<PopoverItem
|
||||
disabled={disableDelete}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
import type { WorkflowLog } from '@/stores/logs/filters/types'
|
||||
|
||||
interface LogRowContextMenuProps {
|
||||
@@ -50,7 +56,7 @@ export function LogRowContextMenu({
|
||||
}}
|
||||
/>
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
{/* Copy Execution ID */}
|
||||
{/* Copy action */}
|
||||
<PopoverItem
|
||||
disabled={!hasExecutionId}
|
||||
onClick={() => {
|
||||
@@ -61,7 +67,8 @@ export function LogRowContextMenu({
|
||||
Copy Execution ID
|
||||
</PopoverItem>
|
||||
|
||||
{/* Open Workflow */}
|
||||
{/* Navigation */}
|
||||
<PopoverDivider />
|
||||
<PopoverItem
|
||||
disabled={!hasWorkflow}
|
||||
onClick={() => {
|
||||
@@ -72,7 +79,8 @@ export function LogRowContextMenu({
|
||||
Open Workflow
|
||||
</PopoverItem>
|
||||
|
||||
{/* Filter by Workflow - only show when not already filtered by this workflow */}
|
||||
{/* Filter actions */}
|
||||
<PopoverDivider />
|
||||
{!isFilteredByThisWorkflow && (
|
||||
<PopoverItem
|
||||
disabled={!hasWorkflow}
|
||||
@@ -84,8 +92,6 @@ export function LogRowContextMenu({
|
||||
Filter by Workflow
|
||||
</PopoverItem>
|
||||
)}
|
||||
|
||||
{/* Clear All Filters - show when any filters are active */}
|
||||
{hasActiveFilters && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { LogRowContextMenu } from './log-row-context-menu'
|
||||
export { OutputContextMenu } from './output-context-menu'
|
||||
export { PrettierOutput } from './prettier-output'
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
import type { ConsoleEntry } from '@/stores/terminal'
|
||||
|
||||
interface ContextMenuPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface TerminalFilters {
|
||||
blockIds: Set<string>
|
||||
statuses: Set<'error' | 'info'>
|
||||
runIds: Set<string>
|
||||
}
|
||||
|
||||
interface LogRowContextMenuProps {
|
||||
isOpen: boolean
|
||||
position: ContextMenuPosition
|
||||
menuRef: RefObject<HTMLDivElement | null>
|
||||
onClose: () => void
|
||||
entry: ConsoleEntry | null
|
||||
filters: TerminalFilters
|
||||
onFilterByBlock: (blockId: string) => void
|
||||
onFilterByStatus: (status: 'error' | 'info') => void
|
||||
onFilterByRunId: (runId: string) => void
|
||||
onClearFilters: () => void
|
||||
onClearConsole: () => void
|
||||
hasActiveFilters: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu for terminal log rows (left side).
|
||||
* Displays filtering options based on the selected row's properties.
|
||||
*/
|
||||
export function LogRowContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
menuRef,
|
||||
onClose,
|
||||
entry,
|
||||
filters,
|
||||
onFilterByBlock,
|
||||
onFilterByStatus,
|
||||
onFilterByRunId,
|
||||
onClearFilters,
|
||||
onClearConsole,
|
||||
hasActiveFilters,
|
||||
}: LogRowContextMenuProps) {
|
||||
const hasRunId = entry?.executionId != null
|
||||
|
||||
const isBlockFiltered = entry ? filters.blockIds.has(entry.blockId) : false
|
||||
const entryStatus = entry?.success ? 'info' : 'error'
|
||||
const isStatusFiltered = entry ? filters.statuses.has(entryStatus) : false
|
||||
const isRunIdFiltered = entry?.executionId ? filters.runIds.has(entry.executionId) : false
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onClose}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
}}
|
||||
/>
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
{/* Clear filters at top when active */}
|
||||
{hasActiveFilters && (
|
||||
<>
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onClearFilters()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Clear All Filters
|
||||
</PopoverItem>
|
||||
{entry && <PopoverDivider />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Filter actions */}
|
||||
{entry && (
|
||||
<>
|
||||
<PopoverItem
|
||||
showCheck={isBlockFiltered}
|
||||
onClick={() => {
|
||||
onFilterByBlock(entry.blockId)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Filter by Block
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
showCheck={isStatusFiltered}
|
||||
onClick={() => {
|
||||
onFilterByStatus(entryStatus)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Filter by Status
|
||||
</PopoverItem>
|
||||
{hasRunId && (
|
||||
<PopoverItem
|
||||
showCheck={isRunIdFiltered}
|
||||
onClick={() => {
|
||||
onFilterByRunId(entry.executionId!)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Filter by Run ID
|
||||
</PopoverItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Destructive action */}
|
||||
{(entry || hasActiveFilters) && <PopoverDivider />}
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onClearConsole()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Clear Console
|
||||
</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
'use client'
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
|
||||
interface ContextMenuPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface OutputContextMenuProps {
|
||||
isOpen: boolean
|
||||
position: ContextMenuPosition
|
||||
menuRef: RefObject<HTMLDivElement | null>
|
||||
onClose: () => void
|
||||
onCopySelection: () => void
|
||||
onCopyAll: () => void
|
||||
onSearch: () => void
|
||||
wrapText: boolean
|
||||
onToggleWrap: () => void
|
||||
openOnRun: boolean
|
||||
onToggleOpenOnRun: () => void
|
||||
onClearConsole: () => void
|
||||
hasSelection: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu for terminal output panel (right side).
|
||||
* Displays copy, search, and display options for the code viewer.
|
||||
*/
|
||||
export function OutputContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
menuRef,
|
||||
onClose,
|
||||
onCopySelection,
|
||||
onCopyAll,
|
||||
onSearch,
|
||||
wrapText,
|
||||
onToggleWrap,
|
||||
openOnRun,
|
||||
onToggleOpenOnRun,
|
||||
onClearConsole,
|
||||
hasSelection,
|
||||
}: OutputContextMenuProps) {
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={onClose}
|
||||
variant='secondary'
|
||||
size='sm'
|
||||
colorScheme='inverted'
|
||||
>
|
||||
<PopoverAnchor
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
}}
|
||||
/>
|
||||
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
|
||||
{/* Copy and search actions */}
|
||||
<PopoverItem
|
||||
disabled={!hasSelection}
|
||||
onClick={() => {
|
||||
onCopySelection()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Copy Selection
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onCopyAll()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Copy All
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onSearch()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</PopoverItem>
|
||||
|
||||
{/* Display settings - toggles don't close menu */}
|
||||
<PopoverDivider />
|
||||
<PopoverItem showCheck={wrapText} onClick={onToggleWrap}>
|
||||
Wrap Text
|
||||
</PopoverItem>
|
||||
<PopoverItem showCheck={openOnRun} onClick={onToggleOpenOnRun}>
|
||||
Open on Run
|
||||
</PopoverItem>
|
||||
|
||||
{/* Destructive action */}
|
||||
<PopoverDivider />
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onClearConsole()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Clear Console
|
||||
</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -38,11 +38,16 @@ import {
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
|
||||
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
||||
import {
|
||||
LogRowContextMenu,
|
||||
OutputContextMenu,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components'
|
||||
import {
|
||||
useOutputPanelResize,
|
||||
useTerminalFilters,
|
||||
useTerminalResize,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks'
|
||||
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { OUTPUT_PANEL_WIDTH, TERMINAL_HEIGHT } from '@/stores/constants'
|
||||
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
|
||||
@@ -365,6 +370,28 @@ export function Terminal() {
|
||||
hasActiveFilters,
|
||||
} = useTerminalFilters()
|
||||
|
||||
// Context menu state
|
||||
const [hasSelection, setHasSelection] = useState(false)
|
||||
const [contextMenuEntry, setContextMenuEntry] = useState<ConsoleEntry | null>(null)
|
||||
const [storedSelectionText, setStoredSelectionText] = useState('')
|
||||
|
||||
// Context menu hooks
|
||||
const {
|
||||
isOpen: isLogRowMenuOpen,
|
||||
position: logRowMenuPosition,
|
||||
menuRef: logRowMenuRef,
|
||||
handleContextMenu: handleLogRowContextMenu,
|
||||
closeMenu: closeLogRowMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
const {
|
||||
isOpen: isOutputMenuOpen,
|
||||
position: outputMenuPosition,
|
||||
menuRef: outputMenuRef,
|
||||
handleContextMenu: handleOutputContextMenu,
|
||||
closeMenu: closeOutputMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
/**
|
||||
* Expands the terminal to its last meaningful height, with safeguards:
|
||||
* - Never expands below {@link DEFAULT_EXPANDED_HEIGHT}.
|
||||
@@ -511,15 +538,11 @@ export function Terminal() {
|
||||
const handleRowClick = useCallback((entry: ConsoleEntry) => {
|
||||
setSelectedEntry((prev) => {
|
||||
const isDeselecting = prev?.id === entry.id
|
||||
// Re-enable auto-select when deselecting, disable when selecting
|
||||
setAutoSelectEnabled(isDeselecting)
|
||||
return isDeselecting ? null : entry
|
||||
})
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Handle header click - toggle between expanded and collapsed
|
||||
*/
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
if (isExpanded) {
|
||||
setIsToggling(true)
|
||||
@@ -529,16 +552,10 @@ export function Terminal() {
|
||||
}
|
||||
}, [expandToLastHeight, isExpanded, setTerminalHeight])
|
||||
|
||||
/**
|
||||
* Handle transition end - reset toggling state
|
||||
*/
|
||||
const handleTransitionEnd = useCallback(() => {
|
||||
setIsToggling(false)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Handle copy output to clipboard
|
||||
*/
|
||||
const handleCopy = useCallback(() => {
|
||||
if (!selectedEntry) return
|
||||
|
||||
@@ -560,9 +577,6 @@ export function Terminal() {
|
||||
}
|
||||
}, [activeWorkflowId, clearWorkflowConsole])
|
||||
|
||||
/**
|
||||
* Activates output search and focuses the search input.
|
||||
*/
|
||||
const activateOutputSearch = useCallback(() => {
|
||||
setIsOutputSearchActive(true)
|
||||
setTimeout(() => {
|
||||
@@ -570,9 +584,6 @@ export function Terminal() {
|
||||
}, 0)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Closes output search and clears the query.
|
||||
*/
|
||||
const closeOutputSearch = useCallback(() => {
|
||||
setIsOutputSearchActive(false)
|
||||
setOutputSearchQuery('')
|
||||
@@ -604,9 +615,6 @@ export function Terminal() {
|
||||
setCurrentMatchIndex(0)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Handle clear console for current workflow via mouse interaction.
|
||||
*/
|
||||
const handleClearConsole = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -615,10 +623,6 @@ export function Terminal() {
|
||||
[clearCurrentWorkflowConsole]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle export of console entries for the current workflow via mouse interaction.
|
||||
* Mirrors the visibility and interaction behavior of the clear console action.
|
||||
*/
|
||||
const handleExportConsole = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -629,9 +633,60 @@ export function Terminal() {
|
||||
[activeWorkflowId, exportConsoleCSV]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle training button click - toggle training state or open modal
|
||||
*/
|
||||
const handleCopySelection = useCallback(() => {
|
||||
if (storedSelectionText) {
|
||||
navigator.clipboard.writeText(storedSelectionText)
|
||||
setShowCopySuccess(true)
|
||||
}
|
||||
}, [storedSelectionText])
|
||||
|
||||
const handleOutputPanelContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const selection = window.getSelection()
|
||||
const selectionText = selection?.toString() || ''
|
||||
setStoredSelectionText(selectionText)
|
||||
setHasSelection(selectionText.length > 0)
|
||||
handleOutputContextMenu(e)
|
||||
},
|
||||
[handleOutputContextMenu]
|
||||
)
|
||||
|
||||
const handleRowContextMenu = useCallback(
|
||||
(e: React.MouseEvent, entry: ConsoleEntry) => {
|
||||
setContextMenuEntry(entry)
|
||||
handleLogRowContextMenu(e)
|
||||
},
|
||||
[handleLogRowContextMenu]
|
||||
)
|
||||
|
||||
const handleFilterByBlock = useCallback(
|
||||
(blockId: string) => {
|
||||
toggleBlock(blockId)
|
||||
closeLogRowMenu()
|
||||
},
|
||||
[toggleBlock, closeLogRowMenu]
|
||||
)
|
||||
|
||||
const handleFilterByStatus = useCallback(
|
||||
(status: 'error' | 'info') => {
|
||||
toggleStatus(status)
|
||||
closeLogRowMenu()
|
||||
},
|
||||
[toggleStatus, closeLogRowMenu]
|
||||
)
|
||||
|
||||
const handleFilterByRunId = useCallback(
|
||||
(runId: string) => {
|
||||
toggleRunId(runId)
|
||||
closeLogRowMenu()
|
||||
},
|
||||
[toggleRunId, closeLogRowMenu]
|
||||
)
|
||||
|
||||
const handleClearConsoleFromMenu = useCallback(() => {
|
||||
clearCurrentWorkflowConsole()
|
||||
}, [clearCurrentWorkflowConsole])
|
||||
|
||||
const handleTrainingClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -644,9 +699,6 @@ export function Terminal() {
|
||||
[isTraining, stopTraining, toggleTrainingModal]
|
||||
)
|
||||
|
||||
/**
|
||||
* Whether training controls should be visible
|
||||
*/
|
||||
const shouldShowTrainingButton = isTrainingEnvEnabled && showTrainingControls
|
||||
|
||||
/**
|
||||
@@ -721,6 +773,23 @@ export function Terminal() {
|
||||
}
|
||||
}, [showCopySuccess])
|
||||
|
||||
/**
|
||||
* Track text selection state for context menu.
|
||||
* Skip updates when the context menu is open to prevent the selection
|
||||
* state from changing mid-click (which would disable the copy button).
|
||||
*/
|
||||
useEffect(() => {
|
||||
const handleSelectionChange = () => {
|
||||
if (isOutputMenuOpen) return
|
||||
|
||||
const selection = window.getSelection()
|
||||
setHasSelection(Boolean(selection && selection.toString().length > 0))
|
||||
}
|
||||
|
||||
document.addEventListener('selectionchange', handleSelectionChange)
|
||||
return () => document.removeEventListener('selectionchange', handleSelectionChange)
|
||||
}, [isOutputMenuOpen])
|
||||
|
||||
/**
|
||||
* Auto-select the latest entry when new logs arrive
|
||||
* Re-enables auto-selection when all entries are cleared
|
||||
@@ -1311,6 +1380,7 @@ export function Terminal() {
|
||||
isSelected && 'bg-[var(--surface-6)] dark:bg-[var(--surface-4)]'
|
||||
)}
|
||||
onClick={() => handleRowClick(entry)}
|
||||
onContextMenu={(e) => handleRowContextMenu(e, entry)}
|
||||
>
|
||||
{/* Block */}
|
||||
<div
|
||||
@@ -1327,7 +1397,13 @@ export function Terminal() {
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className={clsx(COLUMN_WIDTHS.STATUS, COLUMN_BASE_CLASS)}>
|
||||
<div
|
||||
className={clsx(
|
||||
COLUMN_WIDTHS.STATUS,
|
||||
COLUMN_BASE_CLASS,
|
||||
'flex items-center'
|
||||
)}
|
||||
>
|
||||
{statusInfo ? (
|
||||
<Badge variant={statusInfo.isError ? 'red' : 'gray'} dot>
|
||||
{statusInfo.label}
|
||||
@@ -1719,7 +1795,10 @@ export function Terminal() {
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className={clsx('flex-1 overflow-y-auto', !wrapText && 'overflow-x-auto')}>
|
||||
<div
|
||||
className={clsx('flex-1 overflow-y-auto', !wrapText && 'overflow-x-auto')}
|
||||
onContextMenu={handleOutputPanelContextMenu}
|
||||
>
|
||||
{shouldShowCodeDisplay ? (
|
||||
<OutputCodeContent
|
||||
code={selectedEntry.input.code}
|
||||
@@ -1748,6 +1827,42 @@ export function Terminal() {
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Log Row Context Menu */}
|
||||
<LogRowContextMenu
|
||||
isOpen={isLogRowMenuOpen}
|
||||
position={logRowMenuPosition}
|
||||
menuRef={logRowMenuRef}
|
||||
onClose={closeLogRowMenu}
|
||||
entry={contextMenuEntry}
|
||||
filters={filters}
|
||||
onFilterByBlock={handleFilterByBlock}
|
||||
onFilterByStatus={handleFilterByStatus}
|
||||
onFilterByRunId={handleFilterByRunId}
|
||||
onClearFilters={() => {
|
||||
clearFilters()
|
||||
closeLogRowMenu()
|
||||
}}
|
||||
onClearConsole={handleClearConsoleFromMenu}
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
/>
|
||||
|
||||
{/* Output Panel Context Menu */}
|
||||
<OutputContextMenu
|
||||
isOpen={isOutputMenuOpen}
|
||||
position={outputMenuPosition}
|
||||
menuRef={outputMenuRef}
|
||||
onClose={closeOutputMenu}
|
||||
onCopySelection={handleCopySelection}
|
||||
onCopyAll={handleCopy}
|
||||
onSearch={activateOutputSearch}
|
||||
wrapText={wrapText}
|
||||
onToggleWrap={() => setWrapText(!wrapText)}
|
||||
openOnRun={openOnRun}
|
||||
onToggleOpenOnRun={() => setOpenOnRun(!openOnRun)}
|
||||
onClearConsole={handleClearConsoleFromMenu}
|
||||
hasSelection={hasSelection}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,10 +119,8 @@ const STYLES = {
|
||||
'hover:bg-[var(--border-1)] hover:text-[var(--text-primary)] hover:[&_svg]:text-[var(--text-primary)]',
|
||||
},
|
||||
secondary: {
|
||||
active:
|
||||
'bg-[var(--brand-secondary)] text-[var(--text-inverse)] [&_svg]:text-[var(--text-inverse)]',
|
||||
hover:
|
||||
'hover:bg-[var(--brand-secondary)] hover:text-[var(--text-inverse)] dark:hover:text-[var(--text-inverse)] hover:[&_svg]:text-[var(--text-inverse)] dark:hover:[&_svg]:text-[var(--text-inverse)]',
|
||||
active: 'bg-[var(--brand-secondary)] text-white [&_svg]:text-white',
|
||||
hover: 'hover:bg-[var(--brand-secondary)] hover:text-white hover:[&_svg]:text-white',
|
||||
},
|
||||
inverted: {
|
||||
active:
|
||||
@@ -474,14 +472,20 @@ const PopoverScrollArea = React.forwardRef<HTMLDivElement, PopoverScrollAreaProp
|
||||
PopoverScrollArea.displayName = 'PopoverScrollArea'
|
||||
|
||||
export interface PopoverItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Whether this item is currently active/selected */
|
||||
/**
|
||||
* Whether this item has active/highlighted background styling.
|
||||
* Use for keyboard navigation focus or persistent highlight states.
|
||||
*/
|
||||
active?: boolean
|
||||
/** Only show when not inside any folder */
|
||||
rootOnly?: boolean
|
||||
/** Whether this item is disabled */
|
||||
disabled?: boolean
|
||||
/**
|
||||
* Show checkmark when active
|
||||
* Show a checkmark to indicate selection/checked state.
|
||||
* Unlike `active`, this only shows the checkmark without background highlight,
|
||||
* following the pattern where hover provides interaction feedback
|
||||
* and checkmarks indicate current value.
|
||||
* @default false
|
||||
*/
|
||||
showCheck?: boolean
|
||||
@@ -528,7 +532,7 @@ const PopoverItem = React.forwardRef<HTMLDivElement, PopoverItemProps>(
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCheck && active && <Check className={cn('ml-auto', STYLES.size[size].icon)} />}
|
||||
{showCheck && <Check className={cn('ml-auto', STYLES.size[size].icon)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user