diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index a17787657..96e8981e3 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -14,7 +14,7 @@ --panel-width: 320px; /* PANEL_WIDTH.DEFAULT */ --toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */ --editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */ - --terminal-height: 155px; /* TERMINAL_HEIGHT.DEFAULT */ + --terminal-height: 206px; /* TERMINAL_HEIGHT.DEFAULT */ } .sidebar-container { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx index dab65614c..c5988dc23 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx @@ -573,7 +573,19 @@ const TraceSpanNode = memo(function TraceSpanNode({ return children.sort((a, b) => parseTime(a.startTime) - parseTime(b.startTime)) }, [span, spanId, spanStartTime]) - const hasChildren = allChildren.length > 0 + // Hide empty model timing segments for agents without tool calls + const filteredChildren = useMemo(() => { + const isAgent = span.type?.toLowerCase() === 'agent' + const hasToolCalls = + (span.toolCalls?.length ?? 0) > 0 || allChildren.some((c) => c.type?.toLowerCase() === 'tool') + + if (isAgent && !hasToolCalls) { + return allChildren.filter((c) => c.type?.toLowerCase() !== 'model') + } + return allChildren + }, [allChildren, span.type, span.toolCalls]) + + const hasChildren = filteredChildren.length > 0 const isExpanded = isRootWorkflow || expandedNodes.has(spanId) const isToggleable = !isRootWorkflow @@ -685,7 +697,7 @@ const TraceSpanNode = memo(function TraceSpanNode({ {/* Nested Children */} {hasChildren && (
) {
/**
* Applies search highlighting to a single line for virtualized rendering.
+ *
+ * @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 globalMatchOffset - Cumulative match count before this line
+ * @returns Object containing highlighted HTML and count of matches in this line
*/
function applySearchHighlightingToLine(
html: string,
@@ -366,6 +715,8 @@ interface CodeViewerProps {
contentRef?: React.RefObject
/** Enable virtualized rendering for large outputs (uses react-window) */
virtualized?: boolean
+ /** Whether to show a collapse column for JSON folding (only for json language) */
+ showCollapseColumn?: boolean
}
/**
@@ -422,42 +773,39 @@ function applySearchHighlighting(
.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
-}
-
/**
* Props for inner viewer components (with defaults already applied).
*/
type ViewerInnerProps = {
+ /** Code content to display */
code: string
+ /** Whether to show line numbers gutter */
showGutter: boolean
+ /** Language for syntax highlighting */
language: 'javascript' | 'json' | 'python'
+ /** Additional CSS classes for the container */
className?: string
+ /** Left padding offset in pixels */
paddingLeft: number
+ /** Custom styles for the gutter */
gutterStyle?: React.CSSProperties
+ /** Whether to wrap long lines */
wrapText: boolean
+ /** Search query to highlight */
searchQuery?: string
+ /** Index of the current active match */
currentMatchIndex: number
+ /** Callback when match count changes */
onMatchCountChange?: (count: number) => void
+ /** Ref for the content container */
contentRef?: React.RefObject
+ /** Whether to show collapse column for JSON folding */
+ showCollapseColumn: boolean
}
/**
* Virtualized code viewer implementation using react-window.
+ * Optimized for large outputs with efficient scrolling and dynamic row heights.
*/
const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
code,
@@ -471,6 +819,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
currentMatchIndex,
onMatchCountChange,
contentRef,
+ showCollapseColumn,
}: ViewerInnerProps) {
const containerRef = useRef(null)
const listRef = useListRef(null)
@@ -481,52 +830,70 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
key: wrapText ? 'wrap' : 'nowrap',
})
- const matchCount = useMemo(() => countSearchMatches(code, searchQuery || ''), [code, searchQuery])
+ const lines = useMemo(() => code.split('\n'), [code])
+ const gutterWidth = useMemo(() => calculateGutterWidth(lines.length), [lines.length])
+
+ const {
+ collapsedLines,
+ collapsibleLines,
+ collapsedStringLines,
+ visibleLineIndices,
+ toggleCollapse,
+ } = useJsonCollapse(lines, showCollapseColumn, language)
+
+ // Compute display lines (accounting for truncation of collapsed strings)
+ const displayLines = useMemo(() => {
+ return lines.map((line, idx) =>
+ collapsedStringLines.has(idx) ? truncateStringLine(line) : line
+ )
+ }, [lines, collapsedStringLines])
+
+ // Pre-compute cumulative match offsets based on DISPLAYED content (handles truncation)
+ const { matchOffsets, matchCount } = useMemo(() => {
+ if (!searchQuery?.trim()) return { matchOffsets: [], matchCount: 0 }
+
+ const offsets: number[] = []
+ let cumulative = 0
+ const escaped = escapeRegex(searchQuery)
+ const regex = new RegExp(escaped, 'gi')
+ const visibleSet = new Set(visibleLineIndices)
+
+ for (let i = 0; i < lines.length; i++) {
+ offsets.push(cumulative)
+ // Only count matches in visible lines, using displayed (possibly truncated) content
+ if (visibleSet.has(i)) {
+ const matches = displayLines[i].match(regex)
+ cumulative += matches?.length ?? 0
+ }
+ }
+ return { matchOffsets: offsets, matchCount: cumulative }
+ }, [lines.length, displayLines, visibleLineIndices, searchQuery])
useEffect(() => {
onMatchCountChange?.(matchCount)
}, [matchCount, onMatchCountChange])
- const lines = useMemo(() => code.split('\n'), [code])
- const lineCount = lines.length
- const gutterWidth = useMemo(() => calculateGutterWidth(lineCount), [lineCount])
-
- const highlightedLines = useMemo(() => {
+ // Only process visible lines for efficiency (not all lines)
+ const visibleLines = useMemo(() => {
const lang = languages[language] || languages.javascript
- return lines.map((line, idx) => ({
- lineNumber: idx + 1,
- html: highlight(line, lang, language),
- }))
- }, [lines, language])
+ const hasSearch = searchQuery?.trim()
- const matchOffsets = useMemo(() => {
- if (!searchQuery?.trim()) return []
- const offsets: number[] = []
- let cumulative = 0
- const escaped = escapeRegex(searchQuery)
- const regex = new RegExp(escaped, 'gi')
+ return visibleLineIndices.map((idx) => {
+ let html = highlight(displayLines[idx], lang, language)
- for (const line of lines) {
- offsets.push(cumulative)
- const matches = line.match(regex)
- cumulative += matches?.length ?? 0
- }
- return offsets
- }, [lines, searchQuery])
+ if (hasSearch && searchQuery) {
+ const result = applySearchHighlightingToLine(
+ html,
+ searchQuery,
+ currentMatchIndex,
+ matchOffsets[idx]
+ )
+ html = result.html
+ }
- const linesWithSearch = useMemo(() => {
- if (!searchQuery?.trim()) return highlightedLines
-
- return highlightedLines.map((line, idx) => {
- const { html } = applySearchHighlightingToLine(
- line.html,
- searchQuery,
- currentMatchIndex,
- matchOffsets[idx]
- )
- return { ...line, html }
+ return { lineNumber: idx + 1, html }
})
- }, [highlightedLines, searchQuery, currentMatchIndex, matchOffsets])
+ }, [displayLines, language, visibleLineIndices, searchQuery, currentMatchIndex, matchOffsets])
useEffect(() => {
if (!searchQuery?.trim() || matchCount === 0 || !listRef.current) return
@@ -535,12 +902,15 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
for (let i = 0; i < matchOffsets.length; i++) {
const matchesInThisLine = (matchOffsets[i + 1] ?? matchCount) - matchOffsets[i]
if (currentMatchIndex >= accumulated && currentMatchIndex < accumulated + matchesInThisLine) {
- listRef.current.scrollToRow({ index: i, align: 'center' })
+ const visibleIndex = visibleLineIndices.indexOf(i)
+ if (visibleIndex !== -1) {
+ listRef.current.scrollToRow({ index: visibleIndex, align: 'center' })
+ }
break
}
accumulated += matchesInThisLine
}
- }, [currentMatchIndex, searchQuery, matchCount, matchOffsets, listRef])
+ }, [currentMatchIndex, searchQuery, matchCount, matchOffsets, listRef, visibleLineIndices])
useEffect(() => {
const container = containerRef.current
@@ -549,15 +919,11 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
const parent = container.parentElement
if (!parent) return
- const updateHeight = () => {
- setContainerHeight(parent.clientHeight)
- }
-
+ const updateHeight = () => setContainerHeight(parent.clientHeight)
updateHeight()
const resizeObserver = new ResizeObserver(updateHeight)
resizeObserver.observe(parent)
-
return () => resizeObserver.disconnect()
}, [])
@@ -571,7 +937,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
if (rows.length === 0) return
return dynamicRowHeight.observeRowElements(rows)
- }, [wrapText, dynamicRowHeight, linesWithSearch])
+ }, [wrapText, dynamicRowHeight, visibleLines])
const setRefs = useCallback(
(el: HTMLDivElement | null) => {
@@ -583,16 +949,34 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
[contentRef]
)
+ const hasCollapsibleContent = collapsibleLines.size > 0
+ const effectiveShowCollapseColumn = showCollapseColumn && hasCollapsibleContent
+
const rowProps = useMemo(
() => ({
- lines: linesWithSearch,
+ lines: visibleLines,
gutterWidth,
showGutter,
gutterStyle,
leftOffset: paddingLeft,
wrapText,
+ showCollapseColumn: effectiveShowCollapseColumn,
+ collapsibleLines,
+ collapsedLines,
+ onToggleCollapse: toggleCollapse,
}),
- [linesWithSearch, gutterWidth, showGutter, gutterStyle, paddingLeft, wrapText]
+ [
+ visibleLines,
+ gutterWidth,
+ showGutter,
+ gutterStyle,
+ paddingLeft,
+ wrapText,
+ effectiveShowCollapseColumn,
+ collapsibleLines,
+ collapsedLines,
+ toggleCollapse,
+ ]
)
return (
@@ -609,7 +993,7 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
code.split('\n'), [code])
+ const gutterWidth = useMemo(() => calculateGutterWidth(lines.length), [lines.length])
+
+ const {
+ collapsedLines,
+ collapsibleLines,
+ collapsedStringLines,
+ visibleLineIndices,
+ toggleCollapse,
+ } = useJsonCollapse(lines, showCollapseColumn, language)
+
+ // Compute display lines (accounting for truncation of collapsed strings)
+ const displayLines = useMemo(() => {
+ return lines.map((line, idx) =>
+ collapsedStringLines.has(idx) ? truncateStringLine(line) : line
+ )
+ }, [lines, collapsedStringLines])
+
+ // Pre-compute cumulative match offsets based on DISPLAYED content (handles truncation)
+ const { cumulativeMatches, matchCount } = useMemo(() => {
+ if (!searchQuery?.trim()) return { cumulativeMatches: [0], matchCount: 0 }
+
+ const cumulative: number[] = [0]
+ const escaped = escapeRegex(searchQuery)
+ const regex = new RegExp(escaped, 'gi')
+ const visibleSet = new Set(visibleLineIndices)
+
+ for (let i = 0; i < lines.length; i++) {
+ const prev = cumulative[cumulative.length - 1]
+ // Only count matches in visible lines, using displayed content
+ if (visibleSet.has(i)) {
+ const matches = displayLines[i].match(regex)
+ cumulative.push(prev + (matches?.length ?? 0))
+ } else {
+ cumulative.push(prev)
+ }
+ }
+ return { cumulativeMatches: cumulative, matchCount: cumulative[cumulative.length - 1] }
+ }, [lines.length, displayLines, visibleLineIndices, searchQuery])
+
+ useEffect(() => {
+ onMatchCountChange?.(matchCount)
+ }, [matchCount, onMatchCountChange])
+
+ // Pre-compute highlighted lines with search for visible indices (for gutter mode)
+ const highlightedVisibleLines = useMemo(() => {
+ const lang = languages[language] || languages.javascript
+
+ if (!searchQuery?.trim()) {
+ return visibleLineIndices.map((idx) => ({
+ lineNumber: idx + 1,
+ html: highlight(displayLines[idx], lang, language) || ' ',
+ }))
+ }
+
+ return visibleLineIndices.map((idx) => {
+ let html = highlight(displayLines[idx], lang, language)
+ const matchCounter = { count: cumulativeMatches[idx] }
+ html = applySearchHighlighting(html, searchQuery, currentMatchIndex, matchCounter)
+ return { lineNumber: idx + 1, html: html || ' ' }
+ })
+ }, [
+ displayLines,
+ language,
+ visibleLineIndices,
+ searchQuery,
+ currentMatchIndex,
+ cumulativeMatches,
+ ])
+
+ // Pre-compute simple highlighted code (for no-gutter mode)
+ const highlightedCode = useMemo(() => {
+ const lang = languages[language] || languages.javascript
+ const visibleCode = visibleLineIndices.map((idx) => displayLines[idx]).join('\n')
+ let html = highlight(visibleCode, lang, language)
+
+ if (searchQuery?.trim()) {
+ const matchCounter = { count: 0 }
+ html = applySearchHighlighting(html, searchQuery, currentMatchIndex, matchCounter)
+ }
+ return html
+ }, [displayLines, language, visibleLineIndices, searchQuery, currentMatchIndex])
+
+ const whitespaceClass = wrapText ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'
+
+ const hasCollapsibleContent = collapsibleLines.size > 0
+ const effectiveShowCollapseColumn = showCollapseColumn && hasCollapsibleContent
+ const collapseColumnWidth = effectiveShowCollapseColumn ? COLLAPSE_COLUMN_WIDTH : 0
+
+ // Grid-based rendering for gutter alignment (works with wrap)
+ if (showGutter) {
+ return (
+
+
+
+ {highlightedVisibleLines.map(({ lineNumber, html }) => {
+ const idx = lineNumber - 1
+ const isCollapsible = collapsibleLines.has(idx)
+ const isCollapsed = collapsedLines.has(idx)
+
+ return (
+
+
+ {lineNumber}
+
+ {effectiveShowCollapseColumn && (
+
+ {isCollapsible && (
+ toggleCollapse(idx)}
+ />
+ )}
+
+ )}
+
+
+ )
+ })}
+
+
+
+ )
+ }
+
+ // Simple display without gutter
+ return (
+
+
+ 0 ? paddingLeft : undefined }}
+ dangerouslySetInnerHTML={{ __html: highlightedCode }}
+ />
+
+
+ )
+}
+
/**
* 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.
+ * Routes to either standard or virtualized implementation based on the `virtualized` prop.
*
* @example
* ```tsx
@@ -645,162 +1207,6 @@ const VirtualizedViewerInner = memo(function VirtualizedViewerInner({
* />
* ```
*/
-/**
- * Non-virtualized code viewer implementation.
- */
-function ViewerInner({
- code,
- showGutter,
- language,
- className,
- paddingLeft,
- gutterStyle,
- wrapText,
- searchQuery,
- currentMatchIndex,
- onMatchCountChange,
- contentRef,
-}: ViewerInnerProps) {
- // 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'
-
- // Special rendering path: when wrapping with gutter, render per-line rows so gutter stays aligned.
- if (showGutter && wrapText) {
- const lines = code.split('\n')
- const gutterWidth = calculateGutterWidth(lines.length)
- const matchCounter = { count: 0 }
-
- return (
-
-
-
- {lines.map((line, idx) => {
- 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 (
-
-
- {idx + 1}
-
-
-
- )
- })}
-
-
-
- )
- }
-
- // 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 (
-
-
-
-
-
- )
- }
-
- // Calculate line numbers
- const lineCount = code.split('\n').length
- const gutterWidth = calculateGutterWidth(lineCount)
-
- // Render line numbers
- const lineNumbers = []
- for (let i = 1; i <= lineCount; i++) {
- lineNumbers.push(
-
- {i}
-
- )
- }
-
- return (
-
-
- {lineNumbers}
-
-
-
-
-
- )
-}
-
-/**
- * Readonly code viewer with optional gutter and syntax highlighting.
- * Routes to either standard or virtualized implementation based on the `virtualized` prop.
- */
function Viewer({
code,
showGutter = false,
@@ -814,6 +1220,7 @@ function Viewer({
onMatchCountChange,
contentRef,
virtualized = false,
+ showCollapseColumn = false,
}: CodeViewerProps) {
const innerProps: ViewerInnerProps = {
code,
@@ -827,6 +1234,7 @@ function Viewer({
currentMatchIndex,
onMatchCountChange,
contentRef,
+ showCollapseColumn,
}
return virtualized ? :
diff --git a/apps/sim/stores/constants.ts b/apps/sim/stores/constants.ts
index d2eb2bfd3..d22db32c9 100644
--- a/apps/sim/stores/constants.ts
+++ b/apps/sim/stores/constants.ts
@@ -36,7 +36,7 @@ export const PANEL_WIDTH = {
/** Terminal height constraints */
export const TERMINAL_HEIGHT = {
- DEFAULT: 155,
+ DEFAULT: 206,
MIN: 30,
/** Maximum is 70% of viewport, enforced dynamically */
MAX_PERCENTAGE: 0.7,
@@ -58,6 +58,9 @@ export const EDITOR_CONNECTIONS_HEIGHT = {
/** Output panel (terminal execution results) width constraints */
export const OUTPUT_PANEL_WIDTH = {
- DEFAULT: 440,
- MIN: 440,
+ DEFAULT: 560,
+ MIN: 280,
} as const
+
+/** Terminal block column width - minimum width for the logs column */
+export const TERMINAL_BLOCK_COLUMN_WIDTH = 240 as const
diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts
index 0f747f82c..15298c625 100644
--- a/apps/sim/stores/terminal/console/store.ts
+++ b/apps/sim/stores/terminal/console/store.ts
@@ -339,12 +339,49 @@ export const useTerminalConsoleStore = create()(
: update.input
}
+ if (update.isRunning !== undefined) {
+ updatedEntry.isRunning = update.isRunning
+ }
+
+ if (update.isCanceled !== undefined) {
+ updatedEntry.isCanceled = update.isCanceled
+ }
+
+ if (update.iterationCurrent !== undefined) {
+ updatedEntry.iterationCurrent = update.iterationCurrent
+ }
+
+ if (update.iterationTotal !== undefined) {
+ updatedEntry.iterationTotal = update.iterationTotal
+ }
+
+ if (update.iterationType !== undefined) {
+ updatedEntry.iterationType = update.iterationType
+ }
+
return updatedEntry
})
return { entries: updatedEntries }
})
},
+
+ cancelRunningEntries: (workflowId: string) => {
+ set((state) => {
+ const updatedEntries = state.entries.map((entry) => {
+ if (entry.workflowId === workflowId && entry.isRunning) {
+ return {
+ ...entry,
+ isRunning: false,
+ isCanceled: true,
+ endedAt: new Date().toISOString(),
+ }
+ }
+ return entry
+ })
+ return { entries: updatedEntries }
+ })
+ },
}),
{
name: 'terminal-console-store',
diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts
index f496c7356..ca31112eb 100644
--- a/apps/sim/stores/terminal/console/types.ts
+++ b/apps/sim/stores/terminal/console/types.ts
@@ -20,6 +20,10 @@ export interface ConsoleEntry {
iterationCurrent?: number
iterationTotal?: number
iterationType?: SubflowType
+ /** Whether this block is currently running */
+ isRunning?: boolean
+ /** Whether this block execution was canceled */
+ isCanceled?: boolean
}
export interface ConsoleUpdate {
@@ -32,6 +36,14 @@ export interface ConsoleUpdate {
endedAt?: string
durationMs?: number
input?: any
+ /** Whether this block is currently running */
+ isRunning?: boolean
+ /** Whether this block execution was canceled */
+ isCanceled?: boolean
+ /** Iteration context for subflow blocks */
+ iterationCurrent?: number
+ iterationTotal?: number
+ iterationType?: SubflowType
}
export interface ConsoleStore {
@@ -43,6 +55,7 @@ export interface ConsoleStore {
getWorkflowEntries: (workflowId: string) => ConsoleEntry[]
toggleConsole: () => void
updateConsole: (blockId: string, update: string | ConsoleUpdate, executionId?: string) => void
+ cancelRunningEntries: (workflowId: string) => void
_hasHydrated: boolean
setHasHydrated: (hasHydrated: boolean) => void
}
diff --git a/apps/sim/stores/terminal/store.ts b/apps/sim/stores/terminal/store.ts
index faf42d25d..6d8ea91c9 100644
--- a/apps/sim/stores/terminal/store.ts
+++ b/apps/sim/stores/terminal/store.ts
@@ -69,6 +69,15 @@ export const useTerminalStore = create()(
setWrapText: (wrap) => {
set({ wrapText: wrap })
},
+ structuredView: true,
+ /**
+ * Enables or disables structured view mode in the output panel.
+ *
+ * @param structured - Whether output should be displayed as nested blocks.
+ */
+ setStructuredView: (structured) => {
+ set({ structuredView: structured })
+ },
/**
* Indicates whether the terminal store has finished client-side hydration.
*/
diff --git a/apps/sim/stores/terminal/types.ts b/apps/sim/stores/terminal/types.ts
index 8cc3fc307..a50196102 100644
--- a/apps/sim/stores/terminal/types.ts
+++ b/apps/sim/stores/terminal/types.ts
@@ -19,6 +19,8 @@ export interface TerminalState {
setOpenOnRun: (open: boolean) => void
wrapText: boolean
setWrapText: (wrap: boolean) => void
+ structuredView: boolean
+ setStructuredView: (structured: boolean) => void
/**
* Indicates whether the terminal is currently being resized via mouse drag.
*