diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 2c2fc6c38f..c29d149e08 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -52,6 +52,9 @@ const ChatMessageSchema = z.object({ 'gpt-5.1-high', 'gpt-5-codex', 'gpt-5.1-codex', + 'gpt-5.2', + 'gpt-5.2-codex', + 'gpt-5.2-pro', 'gpt-4o', 'gpt-4.1', 'o3', diff --git a/apps/sim/app/api/copilot/user-models/route.ts b/apps/sim/app/api/copilot/user-models/route.ts index d98d49baaa..5e2f22f13d 100644 --- a/apps/sim/app/api/copilot/user-models/route.ts +++ b/apps/sim/app/api/copilot/user-models/route.ts @@ -15,11 +15,14 @@ const DEFAULT_ENABLED_MODELS: Record = { 'gpt-5-medium': false, 'gpt-5-high': false, 'gpt-5.1-fast': false, - 'gpt-5.1': true, - 'gpt-5.1-medium': true, + 'gpt-5.1': false, + 'gpt-5.1-medium': false, 'gpt-5.1-high': false, 'gpt-5-codex': false, - 'gpt-5.1-codex': true, + 'gpt-5.1-codex': false, + 'gpt-5.2': false, + 'gpt-5.2-codex': true, + 'gpt-5.2-pro': true, o3: true, 'claude-4-sonnet': false, 'claude-4.5-haiku': true, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx index 7dfe9af4ea..c55a60e736 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx @@ -2,29 +2,9 @@ import { memo, useEffect, useRef, useState } from 'react' import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' /** - * Minimum delay between characters (fast catch-up mode) + * Character animation delay in milliseconds */ -const MIN_DELAY = 1 - -/** - * Maximum delay between characters (when waiting for content) - */ -const MAX_DELAY = 12 - -/** - * Default delay when streaming normally - */ -const DEFAULT_DELAY = 4 - -/** - * How far behind (in characters) before we speed up - */ -const CATCH_UP_THRESHOLD = 20 - -/** - * How close to content before we slow down - */ -const SLOW_DOWN_THRESHOLD = 5 +const CHARACTER_DELAY = 3 /** * StreamingIndicator shows animated dots during message streaming @@ -54,50 +34,21 @@ interface SmoothStreamingTextProps { isStreaming: boolean } -/** - * Calculates adaptive delay based on how far behind animation is from actual content - * - * @param displayedLength - Current displayed content length - * @param totalLength - Total available content length - * @returns Delay in milliseconds - */ -function calculateAdaptiveDelay(displayedLength: number, totalLength: number): number { - const charsRemaining = totalLength - displayedLength - - if (charsRemaining > CATCH_UP_THRESHOLD) { - // Far behind - speed up to catch up - // Scale from MIN_DELAY to DEFAULT_DELAY based on how far behind - const catchUpFactor = Math.min(1, (charsRemaining - CATCH_UP_THRESHOLD) / 50) - return MIN_DELAY + (DEFAULT_DELAY - MIN_DELAY) * (1 - catchUpFactor) - } - - if (charsRemaining <= SLOW_DOWN_THRESHOLD) { - // Close to content edge - slow down to feel natural - // The closer we are, the slower we go (up to MAX_DELAY) - const slowFactor = 1 - charsRemaining / SLOW_DOWN_THRESHOLD - return DEFAULT_DELAY + (MAX_DELAY - DEFAULT_DELAY) * slowFactor - } - - // Normal streaming speed - return DEFAULT_DELAY -} - /** * SmoothStreamingText component displays text with character-by-character animation - * Creates a smooth streaming effect for AI responses with adaptive speed - * - * Uses adaptive pacing: speeds up when catching up, slows down near content edge + * Creates a smooth streaming effect for AI responses * * @param props - Component props * @returns Streaming text with smooth animation */ export const SmoothStreamingText = memo( ({ content, isStreaming }: SmoothStreamingTextProps) => { - const [displayedContent, setDisplayedContent] = useState('') + // Initialize with full content when not streaming to avoid flash on page load + const [displayedContent, setDisplayedContent] = useState(() => (isStreaming ? '' : content)) const contentRef = useRef(content) - const rafRef = useRef(null) - const indexRef = useRef(0) - const lastFrameTimeRef = useRef(0) + const timeoutRef = useRef(null) + // Initialize index based on streaming state + const indexRef = useRef(isStreaming ? 0 : content.length) const isAnimatingRef = useRef(false) useEffect(() => { @@ -110,42 +61,33 @@ export const SmoothStreamingText = memo( } if (isStreaming) { - if (indexRef.current < content.length && !isAnimatingRef.current) { - isAnimatingRef.current = true - lastFrameTimeRef.current = performance.now() - - const animateText = (timestamp: number) => { + if (indexRef.current < content.length) { + const animateText = () => { const currentContent = contentRef.current const currentIndex = indexRef.current - const elapsed = timestamp - lastFrameTimeRef.current - // Calculate adaptive delay based on how far behind we are - const delay = calculateAdaptiveDelay(currentIndex, currentContent.length) - - if (elapsed >= delay) { - if (currentIndex < currentContent.length) { - const newDisplayed = currentContent.slice(0, currentIndex + 1) - setDisplayedContent(newDisplayed) - indexRef.current = currentIndex + 1 - lastFrameTimeRef.current = timestamp - } - } - - if (indexRef.current < currentContent.length) { - rafRef.current = requestAnimationFrame(animateText) + if (currentIndex < currentContent.length) { + const newDisplayed = currentContent.slice(0, currentIndex + 1) + setDisplayedContent(newDisplayed) + indexRef.current = currentIndex + 1 + timeoutRef.current = setTimeout(animateText, CHARACTER_DELAY) } else { isAnimatingRef.current = false } } - rafRef.current = requestAnimationFrame(animateText) - } else if (indexRef.current < content.length && isAnimatingRef.current) { - // Animation already running, it will pick up new content automatically + if (!isAnimatingRef.current) { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + isAnimatingRef.current = true + animateText() + } } } else { // Streaming ended - show full content immediately - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) } setDisplayedContent(content) indexRef.current = content.length @@ -153,8 +95,8 @@ export const SmoothStreamingText = memo( } return () => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) } isAnimatingRef.current = false } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx index fbb7065f90..ec765dd153 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx @@ -46,12 +46,14 @@ interface SmoothThinkingTextProps { */ const SmoothThinkingText = memo( ({ content, isStreaming }: SmoothThinkingTextProps) => { - const [displayedContent, setDisplayedContent] = useState('') + // Initialize with full content when not streaming to avoid flash on page load + const [displayedContent, setDisplayedContent] = useState(() => (isStreaming ? '' : content)) const [showGradient, setShowGradient] = useState(false) const contentRef = useRef(content) const textRef = useRef(null) const rafRef = useRef(null) - const indexRef = useRef(0) + // Initialize index based on streaming state + const indexRef = useRef(isStreaming ? 0 : content.length) const lastFrameTimeRef = useRef(0) const isAnimatingRef = useRef(false) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 83ff87eb90..65a09e6042 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -1952,7 +1952,12 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: }, [params]) // Skip rendering some internal tools - if (toolCall.name === 'checkoff_todo' || toolCall.name === 'mark_todo_in_progress') return null + if ( + toolCall.name === 'checkoff_todo' || + toolCall.name === 'mark_todo_in_progress' || + toolCall.name === 'tool_search_tool_regex' + ) + return null // Special rendering for subagent tools - show as thinking text with tool calls at top level const SUBAGENT_TOOLS = [ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/model-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/model-selector.tsx index 1ab8d6ecaf..7c639ed010 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/model-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/model-selector.tsx @@ -32,13 +32,6 @@ function getModelIconComponent(modelValue: string) { return } -/** - * Checks if a model should display the MAX badge - */ -function isMaxModel(modelValue: string): boolean { - return modelValue === 'claude-4.5-sonnet' || modelValue === 'claude-4.5-opus' -} - /** * Model selector dropdown for choosing AI model. * Displays model icon and label. @@ -139,11 +132,6 @@ export function ModelSelector({ selectedModel, isNearTop, onModelSelect }: Model > {getModelIconComponent(option.value)} {option.label} - {isMaxModel(option.value) && ( - - MAX - - )} ))} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants.ts index 74c5f275a0..2ad930bad0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/constants.ts @@ -238,8 +238,8 @@ export const MODEL_OPTIONS = [ { value: 'claude-4.5-opus', label: 'Claude 4.5 Opus' }, { value: 'claude-4.5-sonnet', label: 'Claude 4.5 Sonnet' }, { value: 'claude-4.5-haiku', label: 'Claude 4.5 Haiku' }, - { value: 'gpt-5.1-codex', label: 'GPT 5.1 Codex' }, - { value: 'gpt-5.1-medium', label: 'GPT 5.1 Medium' }, + { value: 'gpt-5.2-codex', label: 'GPT 5.2 Codex' }, + { value: 'gpt-5.2-pro', label: 'GPT 5.2 Pro' }, { value: 'gemini-3-pro', label: 'Gemini 3 Pro' }, ] as const diff --git a/apps/sim/lib/copilot/api.ts b/apps/sim/lib/copilot/api.ts index f45cd78660..2eb2cbb30e 100644 --- a/apps/sim/lib/copilot/api.ts +++ b/apps/sim/lib/copilot/api.ts @@ -77,6 +77,9 @@ export interface SendMessageRequest { | 'gpt-5.1-high' | 'gpt-5-codex' | 'gpt-5.1-codex' + | 'gpt-5.2' + | 'gpt-5.2-codex' + | 'gpt-5.2-pro' | 'gpt-4o' | 'gpt-4.1' | 'o3' diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 64d1d3e7bf..2babf87c4b 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -422,7 +422,8 @@ function abortAllInProgressTools(set: any, get: () => CopilotStore) { * Loads messages from DB for UI rendering. * Messages are stored exactly as they render, so we just need to: * 1. Register client tool instances for any tool calls - * 2. Return the messages as-is + * 2. Clear any streaming flags (messages loaded from DB are never actively streaming) + * 3. Return the messages */ function normalizeMessagesForUI(messages: CopilotMessage[]): CopilotMessage[] { try { @@ -438,23 +439,54 @@ function normalizeMessagesForUI(messages: CopilotMessage[]): CopilotMessage[] { } } - // Register client tool instances for all tool calls so they can be looked up + // Register client tool instances and clear streaming flags for all tool calls for (const message of messages) { if (message.contentBlocks) { for (const block of message.contentBlocks as any[]) { if (block?.type === 'tool_call' && block.toolCall) { registerToolCallInstances(block.toolCall) + clearStreamingFlags(block.toolCall) } } } + // Also clear from toolCalls array (legacy format) + if (message.toolCalls) { + for (const toolCall of message.toolCalls) { + clearStreamingFlags(toolCall) + } + } } - // Return messages as-is - they're already in the correct format for rendering return messages } catch { return messages } } +/** + * Recursively clears streaming flags from a tool call and its nested subagent tool calls. + * This ensures messages loaded from DB don't appear to be streaming. + */ +function clearStreamingFlags(toolCall: any): void { + if (!toolCall) return + + // Always set subAgentStreaming to false - messages loaded from DB are never streaming + toolCall.subAgentStreaming = false + + // Clear nested subagent tool calls + if (Array.isArray(toolCall.subAgentBlocks)) { + for (const block of toolCall.subAgentBlocks) { + if (block?.type === 'subagent_tool_call' && block.toolCall) { + clearStreamingFlags(block.toolCall) + } + } + } + if (Array.isArray(toolCall.subAgentToolCalls)) { + for (const subTc of toolCall.subAgentToolCalls) { + clearStreamingFlags(subTc) + } + } +} + /** * Recursively registers client tool instances for a tool call and its nested subagent tool calls. */ diff --git a/apps/sim/stores/panel/copilot/types.ts b/apps/sim/stores/panel/copilot/types.ts index 0ddb9515f8..996317348b 100644 --- a/apps/sim/stores/panel/copilot/types.ts +++ b/apps/sim/stores/panel/copilot/types.ts @@ -106,6 +106,9 @@ export interface CopilotState { | 'gpt-5.1-high' | 'gpt-5-codex' | 'gpt-5.1-codex' + | 'gpt-5.2' + | 'gpt-5.2-codex' + | 'gpt-5.2-pro' | 'gpt-4o' | 'gpt-4.1' | 'o3'