Skip to content

Commit 5c02d46

Browse files
authored
feat(terminal): structured output (#3026)
* feat(code): collapsed JSON in terminal * improvement(code): addressed comments * feat(terminal): added structured output; improvement(preview): note block * feat(terminal): log view * improvement(terminal): ui/ux * improvement(terminal): default sizing and collapsed width * fix: code colors, terminal large output handling * fix(terminal): structured search * improvement: preivew accuracy, invite-modal admin, logs live
1 parent 8b24047 commit 5c02d46

File tree

35 files changed

+3817
-1716
lines changed

35 files changed

+3817
-1716
lines changed

apps/sim/app/_styles/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
1515
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
1616
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
17-
--terminal-height: 155px; /* TERMINAL_HEIGHT.DEFAULT */
17+
--terminal-height: 206px; /* TERMINAL_HEIGHT.DEFAULT */
1818
}
1919

2020
.sidebar-container {

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,19 @@ const TraceSpanNode = memo(function TraceSpanNode({
573573
return children.sort((a, b) => parseTime(a.startTime) - parseTime(b.startTime))
574574
}, [span, spanId, spanStartTime])
575575

576-
const hasChildren = allChildren.length > 0
576+
// Hide empty model timing segments for agents without tool calls
577+
const filteredChildren = useMemo(() => {
578+
const isAgent = span.type?.toLowerCase() === 'agent'
579+
const hasToolCalls =
580+
(span.toolCalls?.length ?? 0) > 0 || allChildren.some((c) => c.type?.toLowerCase() === 'tool')
581+
582+
if (isAgent && !hasToolCalls) {
583+
return allChildren.filter((c) => c.type?.toLowerCase() !== 'model')
584+
}
585+
return allChildren
586+
}, [allChildren, span.type, span.toolCalls])
587+
588+
const hasChildren = filteredChildren.length > 0
577589
const isExpanded = isRootWorkflow || expandedNodes.has(spanId)
578590
const isToggleable = !isRootWorkflow
579591

@@ -685,7 +697,7 @@ const TraceSpanNode = memo(function TraceSpanNode({
685697
{/* Nested Children */}
686698
{hasChildren && (
687699
<div className='flex min-w-0 flex-col gap-[2px] border-[var(--border)] border-l pl-[10px]'>
688-
{allChildren.map((child, index) => (
700+
{filteredChildren.map((child, index) => (
689701
<div key={child.id || `${spanId}-child-${index}`} className='pl-[6px]'>
690702
<TraceSpanNode
691703
span={child}

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default function Logs() {
7878
// eslint-disable-next-line react-hooks/exhaustive-deps
7979
}, [])
8080

81-
const [isLive, setIsLive] = useState(false)
81+
const [isLive, setIsLive] = useState(true)
8282
const [isVisuallyRefreshing, setIsVisuallyRefreshing] = useState(false)
8383
const [isExporting, setIsExporting] = useState(false)
8484
const isSearchOpenRef = useRef<boolean>(false)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import { useCallback, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import clsx from 'clsx'
6-
import { ChevronDown, RepeatIcon, SplitIcon } from 'lucide-react'
6+
import { RepeatIcon, SplitIcon } from 'lucide-react'
77
import { useShallow } from 'zustand/react/shallow'
8+
import { ChevronDown } from '@/components/emcn'
89
import {
910
FieldItem,
1011
type SchemaField,
@@ -115,9 +116,8 @@ function ConnectionItem({
115116
{hasFields && (
116117
<ChevronDown
117118
className={clsx(
118-
'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100',
119-
'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]',
120-
isExpanded && 'rotate-180'
119+
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
120+
!isExpanded && '-rotate-90'
121121
)}
122122
/>
123123
)}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use client'
2+
3+
import { memo } from 'react'
4+
import clsx from 'clsx'
5+
import { Filter } from 'lucide-react'
6+
import {
7+
Button,
8+
Popover,
9+
PopoverContent,
10+
PopoverDivider,
11+
PopoverItem,
12+
PopoverScrollArea,
13+
PopoverSection,
14+
PopoverTrigger,
15+
} from '@/components/emcn'
16+
import type {
17+
BlockInfo,
18+
TerminalFilters,
19+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
20+
import { getBlockIcon } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils'
21+
22+
/**
23+
* Props for the FilterPopover component
24+
*/
25+
export interface FilterPopoverProps {
26+
open: boolean
27+
onOpenChange: (open: boolean) => void
28+
filters: TerminalFilters
29+
toggleStatus: (status: 'error' | 'info') => void
30+
toggleBlock: (blockId: string) => void
31+
uniqueBlocks: BlockInfo[]
32+
hasActiveFilters: boolean
33+
}
34+
35+
/**
36+
* Filter popover component used in terminal header and output panel
37+
*/
38+
export const FilterPopover = memo(function FilterPopover({
39+
open,
40+
onOpenChange,
41+
filters,
42+
toggleStatus,
43+
toggleBlock,
44+
uniqueBlocks,
45+
hasActiveFilters,
46+
}: FilterPopoverProps) {
47+
return (
48+
<Popover open={open} onOpenChange={onOpenChange} size='sm'>
49+
<PopoverTrigger asChild>
50+
<Button
51+
variant='ghost'
52+
className='!p-1.5 -m-1.5'
53+
onClick={(e) => e.stopPropagation()}
54+
aria-label='Filters'
55+
>
56+
<Filter
57+
className={clsx('h-3 w-3', hasActiveFilters && 'text-[var(--brand-secondary)]')}
58+
/>
59+
</Button>
60+
</PopoverTrigger>
61+
<PopoverContent
62+
side='top'
63+
align='end'
64+
sideOffset={4}
65+
onClick={(e) => e.stopPropagation()}
66+
minWidth={160}
67+
maxWidth={220}
68+
maxHeight={300}
69+
>
70+
<PopoverSection>Status</PopoverSection>
71+
<PopoverItem
72+
active={filters.statuses.has('error')}
73+
showCheck={filters.statuses.has('error')}
74+
onClick={() => toggleStatus('error')}
75+
>
76+
<div
77+
className='h-[6px] w-[6px] rounded-[2px]'
78+
style={{ backgroundColor: 'var(--text-error)' }}
79+
/>
80+
<span className='flex-1'>Error</span>
81+
</PopoverItem>
82+
<PopoverItem
83+
active={filters.statuses.has('info')}
84+
showCheck={filters.statuses.has('info')}
85+
onClick={() => toggleStatus('info')}
86+
>
87+
<div
88+
className='h-[6px] w-[6px] rounded-[2px]'
89+
style={{ backgroundColor: 'var(--terminal-status-info-color)' }}
90+
/>
91+
<span className='flex-1'>Info</span>
92+
</PopoverItem>
93+
94+
{uniqueBlocks.length > 0 && (
95+
<>
96+
<PopoverDivider className='my-[4px]' />
97+
<PopoverSection className='!mt-0'>Blocks</PopoverSection>
98+
<PopoverScrollArea className='max-h-[100px]'>
99+
{uniqueBlocks.map((block) => {
100+
const BlockIcon = getBlockIcon(block.blockType)
101+
const isSelected = filters.blockIds.has(block.blockId)
102+
103+
return (
104+
<PopoverItem
105+
key={block.blockId}
106+
active={isSelected}
107+
showCheck={isSelected}
108+
onClick={() => toggleBlock(block.blockId)}
109+
>
110+
{BlockIcon && <BlockIcon className='h-3 w-3' />}
111+
<span className='flex-1'>{block.blockName}</span>
112+
</PopoverItem>
113+
)
114+
})}
115+
</PopoverScrollArea>
116+
</>
117+
)}
118+
</PopoverContent>
119+
</Popover>
120+
)
121+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { FilterPopover, type FilterPopoverProps } from './filter-popover'
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
export { LogRowContextMenu } from './log-row-context-menu'
2-
export { OutputContextMenu } from './output-context-menu'
1+
export { FilterPopover, type FilterPopoverProps } from './filter-popover'
2+
export { LogRowContextMenu, type LogRowContextMenuProps } from './log-row-context-menu'
3+
export { OutputPanel, type OutputPanelProps } from './output-panel'
4+
export { RunningBadge, StatusDisplay, type StatusDisplayProps } from './status-display'
5+
export { ToggleButton, type ToggleButtonProps } from './toggle-button'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { LogRowContextMenu, type LogRowContextMenuProps } from './log-row-context-menu'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/log-row-context-menu.tsx renamed to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/log-row-context-menu/log-row-context-menu.tsx

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
'use client'
22

3-
import type { RefObject } from 'react'
3+
import { memo, type RefObject } from 'react'
44
import {
55
Popover,
66
PopoverAnchor,
77
PopoverContent,
88
PopoverDivider,
99
PopoverItem,
1010
} from '@/components/emcn'
11+
import type {
12+
ContextMenuPosition,
13+
TerminalFilters,
14+
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
1115
import type { ConsoleEntry } from '@/stores/terminal'
1216

13-
interface ContextMenuPosition {
14-
x: number
15-
y: number
16-
}
17-
18-
interface TerminalFilters {
19-
blockIds: Set<string>
20-
statuses: Set<'error' | 'info'>
21-
runIds: Set<string>
22-
}
23-
24-
interface LogRowContextMenuProps {
17+
export interface LogRowContextMenuProps {
2518
isOpen: boolean
2619
position: ContextMenuPosition
2720
menuRef: RefObject<HTMLDivElement | null>
@@ -30,19 +23,16 @@ interface LogRowContextMenuProps {
3023
filters: TerminalFilters
3124
onFilterByBlock: (blockId: string) => void
3225
onFilterByStatus: (status: 'error' | 'info') => void
33-
onFilterByRunId: (runId: string) => void
3426
onCopyRunId: (runId: string) => void
35-
onClearFilters: () => void
3627
onClearConsole: () => void
3728
onFixInCopilot: (entry: ConsoleEntry) => void
38-
hasActiveFilters: boolean
3929
}
4030

4131
/**
4232
* Context menu for terminal log rows (left side).
4333
* Displays filtering options based on the selected row's properties.
4434
*/
45-
export function LogRowContextMenu({
35+
export const LogRowContextMenu = memo(function LogRowContextMenu({
4636
isOpen,
4737
position,
4838
menuRef,
@@ -51,19 +41,15 @@ export function LogRowContextMenu({
5141
filters,
5242
onFilterByBlock,
5343
onFilterByStatus,
54-
onFilterByRunId,
5544
onCopyRunId,
56-
onClearFilters,
5745
onClearConsole,
5846
onFixInCopilot,
59-
hasActiveFilters,
6047
}: LogRowContextMenuProps) {
6148
const hasRunId = entry?.executionId != null
6249

6350
const isBlockFiltered = entry ? filters.blockIds.has(entry.blockId) : false
6451
const entryStatus = entry?.success ? 'info' : 'error'
6552
const isStatusFiltered = entry ? filters.statuses.has(entryStatus) : false
66-
const isRunIdFiltered = entry?.executionId ? filters.runIds.has(entry.executionId) : false
6753

6854
return (
6955
<Popover
@@ -134,34 +120,11 @@ export function LogRowContextMenu({
134120
>
135121
Filter by Status
136122
</PopoverItem>
137-
{hasRunId && (
138-
<PopoverItem
139-
showCheck={isRunIdFiltered}
140-
onClick={() => {
141-
onFilterByRunId(entry.executionId!)
142-
onClose()
143-
}}
144-
>
145-
Filter by Run ID
146-
</PopoverItem>
147-
)}
148123
</>
149124
)}
150125

151-
{/* Clear filters */}
152-
{hasActiveFilters && (
153-
<PopoverItem
154-
onClick={() => {
155-
onClearFilters()
156-
onClose()
157-
}}
158-
>
159-
Clear All Filters
160-
</PopoverItem>
161-
)}
162-
163126
{/* Destructive action */}
164-
{(entry || hasActiveFilters) && <PopoverDivider />}
127+
{entry && <PopoverDivider />}
165128
<PopoverItem
166129
onClick={() => {
167130
onClearConsole()
@@ -173,4 +136,4 @@ export function LogRowContextMenu({
173136
</PopoverContent>
174137
</Popover>
175138
)
176-
}
139+
})

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-context-menu.tsx renamed to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/components/output-context-menu.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
'use client'
22

3-
import type { RefObject } from 'react'
3+
import { memo, type RefObject } from 'react'
44
import {
55
Popover,
66
PopoverAnchor,
77
PopoverContent,
88
PopoverDivider,
99
PopoverItem,
1010
} from '@/components/emcn'
11+
import type { ContextMenuPosition } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
1112

12-
interface ContextMenuPosition {
13-
x: number
14-
y: number
15-
}
16-
17-
interface OutputContextMenuProps {
13+
export interface OutputContextMenuProps {
1814
isOpen: boolean
1915
position: ContextMenuPosition
2016
menuRef: RefObject<HTMLDivElement | null>
2117
onClose: () => void
2218
onCopySelection: () => void
2319
onCopyAll: () => void
2420
onSearch: () => void
21+
structuredView: boolean
22+
onToggleStructuredView: () => void
2523
wrapText: boolean
2624
onToggleWrap: () => void
2725
openOnRun: boolean
@@ -34,14 +32,16 @@ interface OutputContextMenuProps {
3432
* Context menu for terminal output panel (right side).
3533
* Displays copy, search, and display options for the code viewer.
3634
*/
37-
export function OutputContextMenu({
35+
export const OutputContextMenu = memo(function OutputContextMenu({
3836
isOpen,
3937
position,
4038
menuRef,
4139
onClose,
4240
onCopySelection,
4341
onCopyAll,
4442
onSearch,
43+
structuredView,
44+
onToggleStructuredView,
4545
wrapText,
4646
onToggleWrap,
4747
openOnRun,
@@ -96,6 +96,9 @@ export function OutputContextMenu({
9696

9797
{/* Display settings - toggles don't close menu */}
9898
<PopoverDivider />
99+
<PopoverItem showCheck={structuredView} onClick={onToggleStructuredView}>
100+
Structured View
101+
</PopoverItem>
99102
<PopoverItem showCheck={wrapText} onClick={onToggleWrap}>
100103
Wrap Text
101104
</PopoverItem>
@@ -116,4 +119,4 @@ export function OutputContextMenu({
116119
</PopoverContent>
117120
</Popover>
118121
)
119-
}
122+
})

0 commit comments

Comments
 (0)