diff --git a/app/w/components/console/console.tsx b/app/w/components/console/console.tsx
index 37244c6097..e0c07edd57 100644
--- a/app/w/components/console/console.tsx
+++ b/app/w/components/console/console.tsx
@@ -1,15 +1,174 @@
'use client'
-import { useState } from 'react'
-import { PanelLeftClose, PanelLeft, Terminal } from 'lucide-react'
+import { useState, useMemo, useEffect } from 'react'
+import { formatDistanceToNow, format } from 'date-fns'
+import {
+ PanelLeftClose,
+ Terminal,
+ XCircle,
+ Clock,
+ Calendar,
+ CheckCircle2,
+ AlertCircle,
+} from 'lucide-react'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
+import { useConsoleStore } from '@/stores/console/store'
+import { ScrollArea } from '@/components/ui/scroll-area'
+import { Button } from '@/components/ui/button'
+import { ConsoleEntry as ConsoleEntryType } from '@/stores/console/types'
+import { useWorkflowRegistry } from '@/stores/workflow/registry'
+
+function JSONView({ data, level = 0 }: { data: any; level?: number }) {
+ const [isCollapsed, setIsCollapsed] = useState(true)
+
+ if (data === null) return null
+ if (typeof data !== 'object') {
+ return (
+
+ {JSON.stringify(data)}
+
+ )
+ }
+
+ const isArray = Array.isArray(data)
+ const items = isArray ? data : Object.entries(data)
+ const isEmpty = items.length === 0
+
+ if (isEmpty) {
+ return {isArray ? '[]' : '{}'}
+ }
+
+ return (
+
+
{
+ e.stopPropagation()
+ setIsCollapsed(!isCollapsed)
+ }}
+ >
+ {isCollapsed ? '▶' : '▼'} {isArray ? '[' : '{'}
+ {isCollapsed ? '...' : ''}
+
+ {!isCollapsed && (
+
+ {isArray
+ ? items.map((item, index) => (
+
+
+ {index < items.length - 1 && ','}
+
+ ))
+ : (items as [string, any][]).map(([key, value], index) => (
+
+ {key}:{' '}
+
+ {index < items.length - 1 && ','}
+
+ ))}
+
+ )}
+
{isArray ? ']' : '}'}
+
+ )
+}
+
+function ConsoleEntry({ entry }: { entry: ConsoleEntryType }) {
+ const [isExpanded, setIsExpanded] = useState(false)
+
+ const timeAgo = useMemo(
+ () =>
+ formatDistanceToNow(new Date(entry.startedAt), {
+ addSuffix: true,
+ }),
+ [entry.startedAt]
+ )
+
+ return (
+
+
+
+
+
+ {format(new Date(entry.startedAt), 'HH:mm:ss')}
+
+
+
+ Duration: {entry.durationMs}ms
+
+
+
+
+
+
+ {entry.error && (
+
+
+
+
Error
+
{entry.error}
+
+
+ )}
+
+
+
+ )
+}
export function Console() {
const [isCollapsed, setIsCollapsed] = useState(false)
+ const [width, setWidth] = useState(336) // 84 * 4 = 336px (default width)
+ const [isDragging, setIsDragging] = useState(false)
+ const entries = useConsoleStore((state) => state.entries)
+ const clearConsole = useConsoleStore((state) => state.clearConsole)
+ const { activeWorkflowId } = useWorkflowRegistry()
+
+ // Filter entries for active workflow
+ const filteredEntries = useMemo(() => {
+ return entries.filter((entry) => entry.workflowId === activeWorkflowId)
+ }, [entries, activeWorkflowId])
+
+ const handleMouseDown = (e: React.MouseEvent) => {
+ setIsDragging(true)
+ e.preventDefault()
+ }
+
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ if (isDragging) {
+ const newWidth = window.innerWidth - e.clientX
+ setWidth(Math.max(336, Math.min(newWidth, window.innerWidth * 0.8)))
+ }
+ }
+
+ const handleMouseUp = () => {
+ setIsDragging(false)
+ }
+
+ if (isDragging) {
+ document.addEventListener('mousemove', handleMouseMove)
+ document.addEventListener('mouseup', handleMouseUp)
+ }
+
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove)
+ document.removeEventListener('mouseup', handleMouseUp)
+ }
+ }, [isDragging])
if (isCollapsed) {
return (
@@ -29,8 +188,38 @@ export function Console() {
}
return (
-
- {/* Console content will go here */}
+
+
+
+
+
Console
+
+
+
+
+ {filteredEntries.length === 0 ? (
+
+ No console entries
+
+ ) : (
+ filteredEntries.map((entry) => (
+
+ ))
+ )}
+
diff --git a/app/w/hooks/use-workflow-execution.ts b/app/w/hooks/use-workflow-execution.ts
index dbf0dd309c..69a08bddd6 100644
--- a/app/w/hooks/use-workflow-execution.ts
+++ b/app/w/hooks/use-workflow-execution.ts
@@ -5,6 +5,7 @@ import { Executor } from '@/executor'
import { ExecutionResult } from '@/executor/types'
import { useNotificationStore } from '@/stores/notifications/store'
import { useWorkflowRegistry } from '@/stores/workflow/registry'
+import { useConsoleStore } from '@/stores/console/store'
export function useWorkflowExecution() {
const [isExecuting, setIsExecuting] = useState(false)
@@ -12,6 +13,7 @@ export function useWorkflowExecution() {
const { blocks, edges } = useWorkflowStore()
const { activeWorkflowId } = useWorkflowRegistry()
const { addNotification } = useNotificationStore()
+ const { addConsole } = useConsoleStore()
const handleRunWorkflow = useCallback(async () => {
setIsExecuting(true)
@@ -32,6 +34,32 @@ export function useWorkflowExecution() {
const result = await executor.execute('my-run-id')
setExecutionResult(result)
+ // Add console entries for each block execution
+ if (result.logs) {
+ result.logs.forEach((log) => {
+ addConsole({
+ output: log.output,
+ error: log.error,
+ durationMs: log.durationMs,
+ startedAt: log.startedAt,
+ endedAt: log.endedAt,
+ workflowId: activeWorkflowId,
+ timestamp: log.startedAt // Using startedAt as the timestamp
+ })
+ })
+ }
+
+ // Add final execution result to console
+ addConsole({
+ output: result.output,
+ error: result.error,
+ durationMs: result.metadata?.duration || 0,
+ startedAt: result.metadata?.startTime || new Date().toISOString(),
+ endedAt: result.metadata?.endTime || new Date().toISOString(),
+ workflowId: activeWorkflowId,
+ timestamp: result.metadata?.startTime || new Date().toISOString()
+ })
+
if (result.logs) {
console.group('Detailed Block Logs')
result.logs.forEach((log) => {
@@ -74,11 +102,23 @@ export function useWorkflowExecution() {
output: { response: {} },
error: errorMessage
})
+
+ // Add error entry to console
+ addConsole({
+ output: {},
+ error: errorMessage,
+ durationMs: 0,
+ startedAt: new Date().toISOString(),
+ endedAt: new Date().toISOString(),
+ workflowId: activeWorkflowId,
+ timestamp: new Date().toISOString()
+ })
+
addNotification('error', `Workflow execution failed: ${errorMessage}`, activeWorkflowId)
} finally {
setIsExecuting(false)
}
- }, [blocks, edges, addNotification, activeWorkflowId])
+ }, [blocks, edges, addNotification, activeWorkflowId, addConsole])
return { isExecuting, executionResult, handleRunWorkflow }
}
\ No newline at end of file
diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx
new file mode 100644
index 0000000000..0b4a48d87f
--- /dev/null
+++ b/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/package-lock.json b/package-lock.json
index 7790aedb54..955d87b1bd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4",
+ "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
@@ -2531,6 +2532,37 @@
}
}
},
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz",
+ "integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.0",
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-select": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz",
diff --git a/package.json b/package.json
index 5a07c8df3a..3d0116e4f8 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4",
+ "@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
diff --git a/stores/console/store.ts b/stores/console/store.ts
new file mode 100644
index 0000000000..6dcf2df18c
--- /dev/null
+++ b/stores/console/store.ts
@@ -0,0 +1,39 @@
+import { create } from 'zustand'
+import { devtools, persist } from 'zustand/middleware'
+import { ConsoleStore, ConsoleEntry } from './types'
+
+const MAX_ENTRIES = 50
+
+export const useConsoleStore = create()(
+ devtools(
+ persist(
+ (set, get) => ({
+ entries: [],
+
+ addConsole: (entry) => {
+ set((state) => {
+ const newEntry: ConsoleEntry = {
+ ...entry,
+ id: crypto.randomUUID(),
+ timestamp: new Date().toISOString(),
+ }
+
+ // Keep only the last MAX_ENTRIES
+ const newEntries = [newEntry, ...state.entries].slice(0, MAX_ENTRIES)
+
+ return { entries: newEntries }
+ })
+ },
+
+ clearConsole: () => set({ entries: [] }),
+
+ getWorkflowEntries: (workflowId) => {
+ return get().entries.filter((entry) => entry.workflowId === workflowId)
+ },
+ }),
+ {
+ name: 'console-store',
+ }
+ )
+ )
+)
\ No newline at end of file
diff --git a/stores/console/types.ts b/stores/console/types.ts
new file mode 100644
index 0000000000..d1ed6f7160
--- /dev/null
+++ b/stores/console/types.ts
@@ -0,0 +1,17 @@
+export interface ConsoleEntry {
+ id: string
+ output: any
+ error?: string
+ durationMs: number
+ startedAt: string
+ endedAt: string
+ workflowId?: string | null
+ timestamp: string
+}
+
+export interface ConsoleStore {
+ entries: ConsoleEntry[]
+ addConsole: (entry: Omit) => void
+ clearConsole: () => void
+ getWorkflowEntries: (workflowId: string) => ConsoleEntry[]
+}
\ No newline at end of file