Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/sim/app/api/logs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export async function GET(request: NextRequest) {
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
level: workflowExecutionLogs.level,
status: workflowExecutionLogs.status,
trigger: workflowExecutionLogs.trigger,
startedAt: workflowExecutionLogs.startedAt,
endedAt: workflowExecutionLogs.endedAt,
Expand Down Expand Up @@ -78,6 +79,7 @@ export async function GET(request: NextRequest) {
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
level: workflowExecutionLogs.level,
status: workflowExecutionLogs.status,
trigger: workflowExecutionLogs.trigger,
startedAt: workflowExecutionLogs.startedAt,
endedAt: workflowExecutionLogs.endedAt,
Expand Down Expand Up @@ -332,6 +334,7 @@ export async function GET(request: NextRequest) {
deploymentVersion: log.deploymentVersion ?? null,
deploymentVersionName: log.deploymentVersionName ?? null,
level: log.level,
status: log.status,
duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null,
trigger: log.trigger,
createdAt: log.startedAt.toISOString(),
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
await PauseResumeManager.processQueuedResumes(executionId)
}

if (result.error === 'Workflow execution was cancelled') {
if (result.status === 'cancelled') {
logger.info(`[${requestId}] Workflow execution was cancelled`)
sendEvent({
type: 'execution:cancelled',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
import { FileCards, FrozenCanvas, TraceSpans } from '@/app/workspace/[workspaceId]/logs/components'
import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks'
import type { LogStatus } from '@/app/workspace/[workspaceId]/logs/utils'
import { formatDate, StatusBadge, TriggerBadge } from '@/app/workspace/[workspaceId]/logs/utils'
import {
formatDate,
getDisplayStatus,
StatusBadge,
TriggerBadge,
} from '@/app/workspace/[workspaceId]/logs/utils'
import { formatCost } from '@/providers/utils'
import type { WorkflowLog } from '@/stores/logs/filters/types'
import { useLogDetailsUIStore } from '@/stores/logs/store'
Expand Down Expand Up @@ -100,14 +104,7 @@ export const LogDetails = memo(function LogDetails({
[log?.createdAt]
)

const logStatus: LogStatus = useMemo(() => {
if (!log) return 'info'
const baseLevel = (log.level || 'info').toLowerCase()
const isError = baseLevel === 'error'
const isPending = !isError && log.hasPendingPause === true
const isRunning = !isError && !isPending && log.duration === null
return isError ? 'error' : isPending ? 'pending' : isRunning ? 'running' : 'info'
}, [log])
const logStatus = useMemo(() => getDisplayStatus(log?.status), [log?.status])

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import Link from 'next/link'
import { List, type RowComponentProps, useListRef } from 'react-window'
import { Badge, buttonVariants } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import {
formatDate,
formatDuration,
getDisplayStatus,
StatusBadge,
TriggerBadge,
} from '@/app/workspace/[workspaceId]/logs/utils'
import type { WorkflowLog } from '@/stores/logs/filters/types'
import { formatDate, formatDuration, StatusBadge, TriggerBadge } from '../../utils'

const LOG_ROW_HEIGHT = 44 as const

Expand All @@ -25,10 +31,6 @@ interface LogRowProps {
const LogRow = memo(
function LogRow({ log, isSelected, onClick, selectedRowRef }: LogRowProps) {
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
const baseLevel = (log.level || 'info').toLowerCase()
const isError = baseLevel === 'error'
const isPending = !isError && log.hasPendingPause === true
const isRunning = !isError && !isPending && log.duration === null

const handleClick = useCallback(() => onClick(log), [onClick, log])

Expand All @@ -54,9 +56,7 @@ const LogRow = memo(

{/* Status */}
<div className='w-[12%] min-w-[100px]'>
<StatusBadge
status={isError ? 'error' : isPending ? 'pending' : isRunning ? 'running' : 'info'}
/>
<StatusBadge status={getDisplayStatus(log.status)} />
</div>

{/* Workflow */}
Expand Down Expand Up @@ -93,7 +93,7 @@ const LogRow = memo(
</div>

{/* Resume Link */}
{isPending && log.executionId && (log.workflow?.id || log.workflowId) && (
{log.status === 'pending' && log.executionId && (log.workflow?.id || log.workflowId) && (
<Link
href={`/resume/${log.workflow?.id || log.workflowId}/${log.executionId}`}
target='_blank'
Expand All @@ -115,8 +115,7 @@ const LogRow = memo(
return (
prevProps.log.id === nextProps.log.id &&
prevProps.log.duration === nextProps.log.duration &&
prevProps.log.level === nextProps.log.level &&
prevProps.log.hasPendingPause === nextProps.log.hasPendingPause &&
prevProps.log.status === nextProps.log.status &&
prevProps.isSelected === nextProps.isSelected
)
}
Expand Down
4 changes: 1 addition & 3 deletions apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,7 @@ export default function Logs() {

const hasStatusChange =
prevLog?.id === updatedLog.id &&
(updatedLog.duration !== prevLog.duration ||
updatedLog.level !== prevLog.level ||
updatedLog.hasPendingPause !== prevLog.hasPendingPause)
(updatedLog.duration !== prevLog.duration || updatedLog.status !== prevLog.status)

if (updatedLog !== selectedLog) {
setSelectedLog(updatedLog)
Expand Down
27 changes: 25 additions & 2 deletions apps/sim/app/workspace/[workspaceId]/logs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,22 @@ import { getBlock } from '@/blocks/registry'
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook'] as const
const RUNNING_COLOR = '#22c55e' as const
const PENDING_COLOR = '#f59e0b' as const

export type LogStatus = 'error' | 'pending' | 'running' | 'info'
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'

export function getDisplayStatus(status: string | null | undefined): LogStatus {
switch (status) {
case 'running':
return 'running'
case 'pending':
return 'pending'
case 'cancelled':
return 'cancelled'
case 'failed':
return 'error'
default:
return 'info'
}
}

/**
* Checks if a hex color is gray/neutral (low saturation) or too light/dark
Expand Down Expand Up @@ -77,6 +91,11 @@ export const StatusBadge = React.memo(({ status }: StatusBadgeProps) => {
color: lightenColor(RUNNING_COLOR, 65),
label: 'Running',
},
cancelled: {
bg: 'var(--terminal-status-info-bg)',
color: 'var(--terminal-status-info-color)',
label: 'Cancelled',
},
info: {
bg: 'var(--terminal-status-info-bg)',
color: 'var(--terminal-status-info-color)',
Expand Down Expand Up @@ -271,6 +290,7 @@ export interface ExecutionLog {
executionId: string
startedAt: string
level: string
status: string
trigger: string
triggerUserId: string | null
triggerInputs?: unknown
Expand All @@ -291,6 +311,7 @@ interface RawLogResponse extends LogWithDuration, LogWithExecutionData {
endedAt?: string
createdAt?: string
level?: string
status?: string
trigger?: string
triggerUserId?: string | null
error?: string
Expand Down Expand Up @@ -331,6 +352,7 @@ export function mapToExecutionLog(log: RawLogResponse): ExecutionLog {
executionId: log.executionId,
startedAt,
level: log.level || 'info',
status: log.status || 'completed',
trigger: log.trigger || 'manual',
triggerUserId: log.triggerUserId || null,
triggerInputs: undefined,
Expand Down Expand Up @@ -365,6 +387,7 @@ export function mapToExecutionLogAlt(log: RawLogResponse): ExecutionLog {
executionId: log.executionId,
startedAt: log.createdAt || log.startedAt || new Date().toISOString(),
level: log.level || 'info',
status: log.status || 'completed',
trigger: log.trigger || 'manual',
triggerUserId: log.triggerUserId || null,
triggerInputs: undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
'use client'

import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { AlertCircle, ArrowDownToLine, ArrowUp, MoreVertical, Paperclip, X } from 'lucide-react'
import {
AlertCircle,
ArrowDownToLine,
ArrowUp,
MoreVertical,
Paperclip,
Square,
X,
} from 'lucide-react'
import {
Badge,
Button,
Expand Down Expand Up @@ -211,7 +219,7 @@ export function Chat() {

const { entries } = useTerminalConsoleStore()
const { isExecuting } = useExecutionStore()
const { handleRunWorkflow } = useWorkflowExecution()
const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution()
const { data: session } = useSession()
const { addToQueue } = useOperationQueue()

Expand All @@ -224,7 +232,7 @@ export function Chat() {
// Refs
const inputRef = useRef<HTMLInputElement>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const streamReaderRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null)

// File upload hook
const {
Expand Down Expand Up @@ -436,10 +444,28 @@ export function Chat() {
useEffect(() => {
return () => {
timeoutRef.current && clearTimeout(timeoutRef.current)
abortControllerRef.current?.abort()
streamReaderRef.current?.cancel()
}
}, [])

// React to execution cancellation from run button
useEffect(() => {
if (!isExecuting && isStreaming) {
const lastMessage = workflowMessages[workflowMessages.length - 1]
if (lastMessage?.isStreaming) {
streamReaderRef.current?.cancel()
streamReaderRef.current = null
finalizeMessageStream(lastMessage.id)
}
}
}, [isExecuting, isStreaming, workflowMessages, finalizeMessageStream])

const handleStopStreaming = useCallback(() => {
streamReaderRef.current?.cancel()
streamReaderRef.current = null
handleCancelExecution()
}, [handleCancelExecution])

/**
* Processes streaming response from workflow execution
* Reads the stream chunk by chunk and updates the message content in real-time
Expand All @@ -449,6 +475,7 @@ export function Chat() {
const processStreamingResponse = useCallback(
async (stream: ReadableStream, responseMessageId: string) => {
const reader = stream.getReader()
streamReaderRef.current = reader
const decoder = new TextDecoder()
let accumulatedContent = ''
let buffer = ''
Expand Down Expand Up @@ -509,8 +536,15 @@ export function Chat() {
}
}
} catch (error) {
logger.error('Error processing stream:', error)
if ((error as Error)?.name !== 'AbortError') {
logger.error('Error processing stream:', error)
}
finalizeMessageStream(responseMessageId)
} finally {
// Only clear ref if it's still our reader (prevents clobbering a new stream)
if (streamReaderRef.current === reader) {
streamReaderRef.current = null
}
focusInput(100)
}
},
Expand Down Expand Up @@ -590,10 +624,6 @@ export function Chat() {
}
setHistoryIndex(-1)

// Reset abort controller
abortControllerRef.current?.abort()
abortControllerRef.current = new AbortController()

const conversationId = getConversationId(activeWorkflowId)

try {
Expand Down Expand Up @@ -1022,22 +1052,31 @@ export function Chat() {
<Paperclip className='!h-3.5 !w-3.5' />
</Badge>

<Button
onClick={handleSendMessage}
disabled={
(!chatMessage.trim() && chatFiles.length === 0) ||
!activeWorkflowId ||
isExecuting
}
className={cn(
'h-[22px] w-[22px] rounded-full p-0 transition-colors',
chatMessage.trim() || chatFiles.length > 0
? '!bg-[var(--c-C0C0C0)] hover:!bg-[var(--c-D0D0D0)]'
: '!bg-[var(--c-C0C0C0)]'
)}
>
<ArrowUp className='h-3.5 w-3.5 text-black' strokeWidth={2.25} />
</Button>
{isStreaming ? (
<Button
onClick={handleStopStreaming}
className='h-[22px] w-[22px] rounded-full p-0 transition-colors !bg-[var(--c-C0C0C0)] hover:!bg-[var(--c-D0D0D0)]'
>
<Square className='h-2.5 w-2.5 fill-black text-black' />
</Button>
) : (
<Button
onClick={handleSendMessage}
disabled={
(!chatMessage.trim() && chatFiles.length === 0) ||
!activeWorkflowId ||
isExecuting
}
className={cn(
'h-[22px] w-[22px] rounded-full p-0 transition-colors',
chatMessage.trim() || chatFiles.length > 0
? '!bg-[var(--c-C0C0C0)] hover:!bg-[var(--c-D0D0D0)]'
: '!bg-[var(--c-C0C0C0)]'
)}
>
<ArrowUp className='h-3.5 w-3.5 text-black' strokeWidth={2.25} />
</Button>
)}
</div>
</div>

Expand Down
Loading
Loading