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
2 changes: 1 addition & 1 deletion apps/docs/content/docs/en/tools/google_books.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"

<BlockInfoCard
type="google_books"
color="#FFFFFF"
color="#E0E0E0"
/>

## Usage Instructions
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/en/tools/s3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Retrieve an object from an AWS S3 bucket
| --------- | ---- | -------- | ----------- |
| `accessKeyId` | string | Yes | Your AWS Access Key ID |
| `secretAccessKey` | string | Yes | Your AWS Secret Access Key |
| `region` | string | No | Optional region override when URL does not include region \(e.g., us-east-1, eu-west-1\) |
| `s3Uri` | string | Yes | S3 Object URL \(e.g., https://bucket.s3.region.amazonaws.com/path/to/file\) |

#### Output
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/en/tools/slack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
| `channel` | string | No | Slack channel ID \(e.g., C1234567890\) |
| `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) |
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
| `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) |
| `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) |
| `files` | file[] | No | Files to attach to the message |

#### Output
Expand Down
5 changes: 5 additions & 0 deletions apps/sim/app/api/wand/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
finalSystemPrompt += currentTimeContext
}

if (generationType === 'cron-expression') {
finalSystemPrompt +=
'\n\nIMPORTANT: Return ONLY the raw cron expression (e.g., "0 9 * * 1-5"). Do NOT wrap it in markdown code blocks, backticks, or quotes. Do NOT include any explanation or text before or after the expression.'
}

if (generationType === 'json-object') {
finalSystemPrompt +=
'\n\nIMPORTANT: Return ONLY the raw JSON object. Do NOT wrap it in markdown code blocks (no ```json or ```). Do NOT include any explanation or text before or after the JSON. The response must start with { and end with }.'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { StatusBarSegment } from './status-bar'
export { default, StatusBar } from './status-bar'
export { StatusBar } from './status-bar'
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface StatusBarSegment {
timestamp: string
}

export function StatusBar({
function StatusBarInner({
segments,
selectedSegmentIndices,
onSegmentClick,
Expand Down Expand Up @@ -127,4 +127,45 @@ export function StatusBar({
)
}

export default memo(StatusBar)
/**
* Custom equality function for StatusBar memo.
* Performs structural comparison of segments array to avoid re-renders
* when poll data returns new object references with identical content.
*/
function areStatusBarPropsEqual(
prev: Parameters<typeof StatusBarInner>[0],
next: Parameters<typeof StatusBarInner>[0]
): boolean {
if (prev.workflowId !== next.workflowId) return false
if (prev.segmentDurationMs !== next.segmentDurationMs) return false
if (prev.preferBelow !== next.preferBelow) return false

if (prev.selectedSegmentIndices !== next.selectedSegmentIndices) {
if (!prev.selectedSegmentIndices || !next.selectedSegmentIndices) return false
if (prev.selectedSegmentIndices.length !== next.selectedSegmentIndices.length) return false
for (let i = 0; i < prev.selectedSegmentIndices.length; i++) {
if (prev.selectedSegmentIndices[i] !== next.selectedSegmentIndices[i]) return false
}
}

if (prev.segments !== next.segments) {
if (prev.segments.length !== next.segments.length) return false
for (let i = 0; i < prev.segments.length; i++) {
const ps = prev.segments[i]
const ns = next.segments[i]
if (
ps.successRate !== ns.successRate ||
ps.hasExecutions !== ns.hasExecutions ||
ps.totalExecutions !== ns.totalExecutions ||
ps.successfulExecutions !== ns.successfulExecutions ||
ps.timestamp !== ns.timestamp
) {
return false
}
}
}

return true
}

export const StatusBar = memo(StatusBarInner, areStatusBarPropsEqual)
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { WorkflowExecutionItem } from './workflows-list'
export { default, WorkflowsList } from './workflows-list'
export { WorkflowsList } from './workflows-list'
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface WorkflowExecutionItem {
overallSuccessRate: number
}

export function WorkflowsList({
function WorkflowsListInner({
filteredExecutions,
expandedWorkflowId,
onToggleWorkflow,
Expand Down Expand Up @@ -103,7 +103,7 @@ export function WorkflowsList({
<StatusBar
segments={workflow.segments}
selectedSegmentIndices={selectedSegments[workflow.workflowId] || null}
onSegmentClick={onSegmentClick as any}
onSegmentClick={onSegmentClick}
workflowId={workflow.workflowId}
segmentDurationMs={segmentDurationMs}
preferBelow={idx < 2}
Expand All @@ -124,4 +124,4 @@ export function WorkflowsList({
)
}

export default memo(WorkflowsList)
export const WorkflowsList = memo(WorkflowsListInner)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Skeleton } from '@/components/ui/skeleton'
import { formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
Expand Down Expand Up @@ -141,31 +141,90 @@ function toWorkflowExecution(wf: WorkflowStats): WorkflowExecution {
}
}

export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
function DashboardInner({ stats, isLoading, error }: DashboardProps) {
const [selectedSegments, setSelectedSegments] = useState<Record<string, number[]>>({})
const [lastAnchorIndices, setLastAnchorIndices] = useState<Record<string, number>>({})
const barsAreaRef = useRef<HTMLDivElement | null>(null)
const lastAnchorIndicesRef = useRef<Record<string, number>>({})

const { workflowIds, searchQuery, toggleWorkflowId, timeRange } = useFilterStore()

const allWorkflows = useWorkflowRegistry((state) => state.workflows)

const expandedWorkflowId = workflowIds.length === 1 ? workflowIds[0] : null

const { executions, aggregateSegments, segmentMs } = useMemo(() => {
const { rawExecutions, aggregateSegments, segmentMs } = useMemo(() => {
if (!stats) {
return { executions: [], aggregateSegments: [], segmentMs: 0 }
return { rawExecutions: [], aggregateSegments: [], segmentMs: 0 }
}

const workflowExecutions = stats.workflows.map(toWorkflowExecution)

return {
executions: workflowExecutions,
rawExecutions: stats.workflows.map(toWorkflowExecution),
aggregateSegments: stats.aggregateSegments,
segmentMs: stats.segmentMs,
}
}, [stats])

/**
* Stabilize execution objects: reuse previous references for workflows
* whose segment data hasn't structurally changed between polls.
* This prevents cascading re-renders through WorkflowsList → StatusBar.
*/
const prevExecutionsRef = useRef<WorkflowExecution[]>([])

const executions = useMemo(() => {
const prevMap = new Map(prevExecutionsRef.current.map((e) => [e.workflowId, e]))
let anyChanged = false

const result = rawExecutions.map((exec) => {
const prev = prevMap.get(exec.workflowId)
if (!prev) {
anyChanged = true
return exec
}
if (
prev.overallSuccessRate !== exec.overallSuccessRate ||
prev.workflowName !== exec.workflowName ||
prev.segments.length !== exec.segments.length
) {
anyChanged = true
return exec
}

for (let i = 0; i < prev.segments.length; i++) {
const ps = prev.segments[i]
const ns = exec.segments[i]
if (
ps.totalExecutions !== ns.totalExecutions ||
ps.successfulExecutions !== ns.successfulExecutions ||
ps.timestamp !== ns.timestamp ||
ps.avgDurationMs !== ns.avgDurationMs ||
ps.p50Ms !== ns.p50Ms ||
ps.p90Ms !== ns.p90Ms ||
ps.p99Ms !== ns.p99Ms
) {
anyChanged = true
return exec
}
}

return prev
})

if (
!anyChanged &&
result.length === prevExecutionsRef.current.length &&
result.every((r, i) => r === prevExecutionsRef.current[i])
) {
return prevExecutionsRef.current
}

return result
}, [rawExecutions])

useEffect(() => {
prevExecutionsRef.current = executions
}, [executions])

const lastExecutionByWorkflow = useMemo(() => {
const map = new Map<string, number>()
for (const wf of executions) {
Expand Down Expand Up @@ -312,6 +371,8 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
[toggleWorkflowId]
)

lastAnchorIndicesRef.current = lastAnchorIndices

/**
* Handles segment click for selecting time segments.
* @param workflowId - The workflow containing the segment
Expand Down Expand Up @@ -361,7 +422,7 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
} else if (mode === 'range') {
setSelectedSegments((prev) => {
const currentSegments = prev[workflowId] || []
const anchor = lastAnchorIndices[workflowId] ?? segmentIndex
const anchor = lastAnchorIndicesRef.current[workflowId] ?? segmentIndex
const [start, end] =
anchor < segmentIndex ? [anchor, segmentIndex] : [segmentIndex, anchor]
const range = Array.from({ length: end - start + 1 }, (_, i) => start + i)
Expand All @@ -370,12 +431,12 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
})
}
},
[lastAnchorIndices]
[]
)

useEffect(() => {
setSelectedSegments({})
setLastAnchorIndices({})
setSelectedSegments((prev) => (Object.keys(prev).length > 0 ? {} : prev))
setLastAnchorIndices((prev) => (Object.keys(prev).length > 0 ? {} : prev))
}, [stats, timeRange, workflowIds, searchQuery])

if (isLoading) {
Expand Down Expand Up @@ -493,7 +554,7 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
</div>
</div>

<div className='min-h-0 flex-1 overflow-hidden' ref={barsAreaRef}>
<div className='min-h-0 flex-1 overflow-hidden'>
<WorkflowsList
filteredExecutions={filteredExecutions as WorkflowExecution[]}
expandedWorkflowId={expandedWorkflowId}
Expand All @@ -507,3 +568,5 @@ export default function Dashboard({ stats, isLoading, error }: DashboardProps) {
</div>
)
}

export default memo(DashboardInner)
Loading