Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import clsx from 'clsx'
import { X } from 'lucide-react'
import { Button, Tooltip } from '@/components/emcn'
import { safelyRenderValue } from '@/lib/core/utils/formatting'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
Expand Down Expand Up @@ -147,7 +148,7 @@ export const Notifications = memo(function Notifications() {
{notification.level === 'error' && (
<span className='mr-[6px] mb-[2.75px] inline-block h-[6px] w-[6px] rounded-[2px] bg-[var(--text-error)] align-middle' />
)}
{notification.message}
{safelyRenderValue(notification.message)}
</div>
<Tooltip.Root>
<Tooltip.Trigger asChild>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useCallback, useState } from 'react'
import { ArrowUp, ChevronDown, ChevronRight, Trash2 } from 'lucide-react'
import { safelyRenderValue } from '@/lib/core/utils/formatting'
import { useCopilotStore } from '@/stores/panel/copilot/store'

/**
Expand Down Expand Up @@ -66,7 +67,9 @@ export function QueuedMessages() {

{/* Message content */}
<div className='min-w-0 flex-1'>
<p className='truncate text-[13px] text-[var(--text-primary)]'>{msg.content}</p>
<p className='truncate text-[13px] text-[var(--text-primary)]'>
{safelyRenderValue(msg.content)}
</p>
</div>

{/* Actions - always visible */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { memo, useEffect, useState } from 'react'
import { Check, ChevronDown, ChevronRight, Loader2, X } from 'lucide-react'
import { Button } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { safelyRenderValue } from '@/lib/core/utils/formatting'

/**
* Represents a single todo item
Expand Down Expand Up @@ -148,7 +149,7 @@ export const TodoList = memo(function TodoList({
: 'text-[var(--text-primary)]'
)}
>
{todo.content}
{safelyRenderValue(todo.content)}
</span>
</div>
))}
Expand Down
43 changes: 43 additions & 0 deletions apps/sim/lib/core/utils/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,46 @@ export function formatDuration(durationMs: number): string {
const remainingMinutes = minutes % 60
return `${hours}h ${remainingMinutes}m`
}

/**
* Safely converts a value to a string for React rendering.
* Prevents React error #31 by handling objects, arrays, and other non-primitive types.
*
* @param value - The value to convert to a string
* @returns A string representation safe for React rendering
*
* @example
* safelyRenderValue("hello") // "hello"
* safelyRenderValue(123) // "123"
* safelyRenderValue({ text: "hello", type: "text" }) // '{"text":"hello","type":"text"}'
* safelyRenderValue([1, 2, 3]) // "[1,2,3]"
* safelyRenderValue(null) // ""
* safelyRenderValue(undefined) // ""
*/
export function safelyRenderValue(value: unknown): string {
if (value === null || value === undefined) {
return ''
}

if (typeof value === 'string') {
return value
}

if (typeof value === 'number' || typeof value === 'boolean') {
return String(value)
}

if (typeof value === 'object') {
if ('text' in value && typeof (value as Record<string, unknown>).text === 'string') {
return (value as Record<string, unknown>).text as string
}

try {
return JSON.stringify(value)
} catch {
return '[Object]'
}
}

return String(value)
}