mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(copilot): diff improvements (#1002)
* Fix abort * Cred updates * Updates * Fix sheet id showing up in diff view * Update diff view * Text overflow * Optimistic accept * Serialization catching * Depth 0 fix * Fix icons * Updates * Lint
This commit is contained in:
committed by
GitHub
parent
d972bab206
commit
d7fd4a9618
@@ -65,6 +65,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
if (!Number.isNaN(limit) && limit > 0 && currentUsage >= limit) {
|
||||
// Usage exceeded
|
||||
logger.info('[API VALIDATION] Usage exceeded', { userId, currentUsage, limit })
|
||||
return new NextResponse(null, { status: 402 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +371,42 @@ export async function POST(req: NextRequest) {
|
||||
(currentChat?.conversationId as string | undefined) || conversationId
|
||||
|
||||
// If we have a conversationId, only send the most recent user message; else send full history
|
||||
const messagesForAgent = effectiveConversationId ? [messages[messages.length - 1]] : messages
|
||||
const latestUserMessage =
|
||||
[...messages].reverse().find((m) => m?.role === 'user') || messages[messages.length - 1]
|
||||
const messagesForAgent = effectiveConversationId ? [latestUserMessage] : messages
|
||||
|
||||
const requestPayload = {
|
||||
messages: messagesForAgent,
|
||||
workflowId,
|
||||
userId: authenticatedUserId,
|
||||
stream: stream,
|
||||
streamToolCalls: true,
|
||||
mode: mode,
|
||||
provider: providerToUse,
|
||||
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
|
||||
...(typeof depth === 'number' ? { depth } : {}),
|
||||
...(session?.user?.name && { userName: session.user.name }),
|
||||
}
|
||||
|
||||
// Log the payload being sent to the streaming endpoint
|
||||
try {
|
||||
logger.info(`[${tracker.requestId}] Sending payload to sim agent streaming endpoint`, {
|
||||
url: `${SIM_AGENT_API_URL}/api/chat-completion-streaming`,
|
||||
provider: providerToUse,
|
||||
mode,
|
||||
stream,
|
||||
workflowId,
|
||||
hasConversationId: !!effectiveConversationId,
|
||||
depth: typeof depth === 'number' ? depth : undefined,
|
||||
messagesCount: requestPayload.messages.length,
|
||||
})
|
||||
// Full payload as JSON string
|
||||
logger.info(
|
||||
`[${tracker.requestId}] Full streaming payload: ${JSON.stringify(requestPayload)}`
|
||||
)
|
||||
} catch (e) {
|
||||
logger.warn(`[${tracker.requestId}] Failed to log payload preview for streaming endpoint`, e)
|
||||
}
|
||||
|
||||
const simAgentResponse = await fetch(`${SIM_AGENT_API_URL}/api/chat-completion-streaming`, {
|
||||
method: 'POST',
|
||||
@@ -379,18 +414,7 @@ export async function POST(req: NextRequest) {
|
||||
'Content-Type': 'application/json',
|
||||
...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: messagesForAgent,
|
||||
workflowId,
|
||||
userId: authenticatedUserId,
|
||||
stream: stream,
|
||||
streamToolCalls: true,
|
||||
mode: mode,
|
||||
provider: providerToUse,
|
||||
...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}),
|
||||
...(typeof depth === 'number' ? { depth } : {}),
|
||||
...(session?.user?.name && { userName: session.user.name }),
|
||||
}),
|
||||
body: JSON.stringify(requestPayload),
|
||||
})
|
||||
|
||||
if (!simAgentResponse.ok) {
|
||||
|
||||
@@ -196,22 +196,17 @@ export function DiffControls() {
|
||||
logger.warn('Failed to clear preview YAML:', error)
|
||||
})
|
||||
|
||||
// Accept changes with automatic backup and rollback on failure
|
||||
await acceptChanges()
|
||||
// Accept changes without blocking the UI; errors will be logged by the store handler
|
||||
acceptChanges().catch((error) => {
|
||||
logger.error('Failed to accept changes (background):', error)
|
||||
})
|
||||
|
||||
logger.info('Successfully accepted and saved workflow changes')
|
||||
// Show success feedback if needed
|
||||
logger.info('Accept triggered; UI will update optimistically')
|
||||
} catch (error) {
|
||||
logger.error('Failed to accept changes:', error)
|
||||
|
||||
// Show error notification to user
|
||||
// Note: The acceptChanges function has already rolled back the state
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
||||
// You could add toast notification here
|
||||
console.error('Workflow update failed:', errorMessage)
|
||||
|
||||
// Optionally show user-facing error dialog
|
||||
alert(`Failed to save workflow changes: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
} from 'react'
|
||||
import {
|
||||
ArrowUp,
|
||||
Boxes,
|
||||
Brain,
|
||||
BrainCircuit,
|
||||
BrainCog,
|
||||
Check,
|
||||
FileText,
|
||||
Image,
|
||||
Infinity as InfinityIcon,
|
||||
Loader2,
|
||||
MessageCircle,
|
||||
Package,
|
||||
@@ -435,14 +435,14 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
}
|
||||
|
||||
const getDepthLabel = () => {
|
||||
if (agentDepth === 0) return 'Lite'
|
||||
if (agentDepth === 0) return 'Fast'
|
||||
if (agentDepth === 1) return 'Auto'
|
||||
if (agentDepth === 2) return 'Pro'
|
||||
return 'Max'
|
||||
}
|
||||
|
||||
const getDepthLabelFor = (value: 0 | 1 | 2 | 3) => {
|
||||
if (value === 0) return 'Lite'
|
||||
if (value === 0) return 'Fast'
|
||||
if (value === 1) return 'Auto'
|
||||
if (value === 2) return 'Pro'
|
||||
return 'Max'
|
||||
@@ -459,9 +459,9 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
|
||||
const getDepthIconFor = (value: 0 | 1 | 2 | 3) => {
|
||||
if (value === 0) return <Zap className='h-3 w-3 text-muted-foreground' />
|
||||
if (value === 1) return <Boxes className='h-3 w-3 text-muted-foreground' />
|
||||
if (value === 2) return <BrainCircuit className='h-3 w-3 text-muted-foreground' />
|
||||
return <BrainCog className='h-3 w-3 text-muted-foreground' />
|
||||
if (value === 1) return <InfinityIcon className='h-3 w-3 text-muted-foreground' />
|
||||
if (value === 2) return <Brain className='h-3 w-3 text-muted-foreground' />
|
||||
return <BrainCircuit className='h-3 w-3 text-muted-foreground' />
|
||||
}
|
||||
|
||||
const getDepthIcon = () => getDepthIconFor(agentDepth)
|
||||
@@ -654,7 +654,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
)}
|
||||
>
|
||||
<span className='flex items-center gap-1.5'>
|
||||
<Boxes className='h-3 w-3 text-muted-foreground' />
|
||||
<InfinityIcon className='h-3 w-3 text-muted-foreground' />
|
||||
Auto
|
||||
</span>
|
||||
{agentDepth === 1 && (
|
||||
@@ -682,7 +682,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
>
|
||||
<span className='flex items-center gap-1.5'>
|
||||
<Zap className='h-3 w-3 text-muted-foreground' />
|
||||
Lite
|
||||
Fast
|
||||
</span>
|
||||
{agentDepth === 0 && (
|
||||
<Check className='h-3 w-3 text-muted-foreground' />
|
||||
@@ -709,7 +709,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
)}
|
||||
>
|
||||
<span className='flex items-center gap-1.5'>
|
||||
<BrainCircuit className='h-3 w-3 text-muted-foreground' />
|
||||
<Brain className='h-3 w-3 text-muted-foreground' />
|
||||
Pro
|
||||
</span>
|
||||
{agentDepth === 2 && (
|
||||
@@ -737,7 +737,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
)}
|
||||
>
|
||||
<span className='flex items-center gap-1.5'>
|
||||
<BrainCog className='h-3 w-3 text-muted-foreground' />
|
||||
<BrainCircuit className='h-3 w-3 text-muted-foreground' />
|
||||
Max
|
||||
</span>
|
||||
{agentDepth === 3 && (
|
||||
|
||||
@@ -34,6 +34,7 @@ interface FileSelectorInputProps {
|
||||
disabled: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, any>
|
||||
}
|
||||
|
||||
export function FileSelectorInput({
|
||||
@@ -42,6 +43,7 @@ export function FileSelectorInput({
|
||||
disabled,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: FileSelectorInputProps) {
|
||||
const { getValue } = useSubBlockStore()
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
@@ -49,6 +51,23 @@ export function FileSelectorInput({
|
||||
const params = useParams()
|
||||
const workflowIdFromUrl = (params?.workflowId as string) || activeWorkflowId || ''
|
||||
|
||||
// Helper to coerce various preview value shapes into a string ID
|
||||
const coerceToIdString = (val: unknown): string => {
|
||||
if (!val) return ''
|
||||
if (typeof val === 'string') return val
|
||||
if (typeof val === 'number') return String(val)
|
||||
if (typeof val === 'object') {
|
||||
const obj = val as Record<string, any>
|
||||
return (obj.id ||
|
||||
obj.fileId ||
|
||||
obj.value ||
|
||||
obj.documentId ||
|
||||
obj.spreadsheetId ||
|
||||
'') as string
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// Use the proper hook to get the current value and setter
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [selectedFileId, setSelectedFileId] = useState<string>('')
|
||||
@@ -108,19 +127,37 @@ export function FileSelectorInput({
|
||||
const isMicrosoftSharePoint = provider === 'microsoft' && subBlock.serviceId === 'sharepoint'
|
||||
const isMicrosoftPlanner = provider === 'microsoft-planner'
|
||||
// For Confluence and Jira, we need the domain and credentials
|
||||
const domain = isConfluence || isJira ? (getValue(blockId, 'domain') as string) || '' : ''
|
||||
const jiraCredential = isJira ? (getValue(blockId, 'credential') as string) || '' : ''
|
||||
const domain =
|
||||
isConfluence || isJira
|
||||
? (isPreview && previewContextValues?.domain?.value) ||
|
||||
(getValue(blockId, 'domain') as string) ||
|
||||
''
|
||||
: ''
|
||||
const jiraCredential = isJira
|
||||
? (isPreview && previewContextValues?.credential?.value) ||
|
||||
(getValue(blockId, 'credential') as string) ||
|
||||
''
|
||||
: ''
|
||||
// For Discord, we need the bot token and server ID
|
||||
const botToken = isDiscord ? (getValue(blockId, 'botToken') as string) || '' : ''
|
||||
const serverId = isDiscord ? (getValue(blockId, 'serverId') as string) || '' : ''
|
||||
const botToken = isDiscord
|
||||
? (isPreview && previewContextValues?.botToken?.value) ||
|
||||
(getValue(blockId, 'botToken') as string) ||
|
||||
''
|
||||
: ''
|
||||
const serverId = isDiscord
|
||||
? (isPreview && previewContextValues?.serverId?.value) ||
|
||||
(getValue(blockId, 'serverId') as string) ||
|
||||
''
|
||||
: ''
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value
|
||||
const value = isPreview ? previewValue : storeValue
|
||||
|
||||
// Keep local selection in sync with store value (and preview)
|
||||
useEffect(() => {
|
||||
const effective = isPreview && previewValue !== undefined ? previewValue : storeValue
|
||||
if (typeof effective === 'string' && effective !== '') {
|
||||
const raw = isPreview && previewValue !== undefined ? previewValue : storeValue
|
||||
const effective = coerceToIdString(raw)
|
||||
if (effective) {
|
||||
if (isJira) {
|
||||
setSelectedIssueId(effective)
|
||||
} else if (isDiscord) {
|
||||
@@ -385,7 +422,7 @@ export function FileSelectorInput({
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<MicrosoftFileSelector
|
||||
value={selectedFileId}
|
||||
value={coerceToIdString(selectedFileId)}
|
||||
onChange={handleFileChange}
|
||||
provider='microsoft-excel'
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
@@ -418,7 +455,7 @@ export function FileSelectorInput({
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<MicrosoftFileSelector
|
||||
value={selectedFileId}
|
||||
value={coerceToIdString(selectedFileId)}
|
||||
onChange={handleFileChange}
|
||||
provider='microsoft-word'
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
@@ -450,7 +487,7 @@ export function FileSelectorInput({
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<MicrosoftFileSelector
|
||||
value={selectedFileId}
|
||||
value={coerceToIdString(selectedFileId)}
|
||||
onChange={handleFileChange}
|
||||
provider='microsoft'
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
@@ -482,7 +519,7 @@ export function FileSelectorInput({
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<MicrosoftFileSelector
|
||||
value={selectedFileId}
|
||||
value={coerceToIdString(selectedFileId)}
|
||||
onChange={handleFileChange}
|
||||
provider='microsoft'
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
@@ -662,11 +699,9 @@ export function FileSelectorInput({
|
||||
// Default to Google Drive picker
|
||||
return (
|
||||
<GoogleDrivePicker
|
||||
value={
|
||||
(isPreview && previewValue !== undefined
|
||||
? (previewValue as string)
|
||||
: (storeValue as string)) || ''
|
||||
}
|
||||
value={coerceToIdString(
|
||||
(isPreview && previewValue !== undefined ? previewValue : storeValue) as any
|
||||
)}
|
||||
onChange={(val, info) => {
|
||||
setSelectedFileId(val)
|
||||
setFileInfo(info || null)
|
||||
@@ -682,7 +717,11 @@ export function FileSelectorInput({
|
||||
onFileInfoChange={setFileInfo}
|
||||
clientId={clientId}
|
||||
apiKey={apiKey}
|
||||
credentialId={(getValue(blockId, 'credential') as string) || ''}
|
||||
credentialId={
|
||||
((isPreview && previewContextValues?.credential?.value) ||
|
||||
(getValue(blockId, 'credential') as string) ||
|
||||
'') as string
|
||||
}
|
||||
workflowId={workflowIdFromUrl}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -389,6 +389,8 @@ export function LongInput({
|
||||
fontFamily: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
height: `${height}px`,
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -397,7 +399,7 @@ export function LongInput({
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
width: textareaRef.current ? `${textareaRef.current.clientWidth}px` : '100%',
|
||||
width: '100%',
|
||||
height: `${height}px`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
|
||||
@@ -55,6 +55,7 @@ interface ToolInputProps {
|
||||
isPreview?: boolean
|
||||
previewValue?: any
|
||||
disabled?: boolean
|
||||
allowExpandInPreview?: boolean
|
||||
}
|
||||
|
||||
interface StoredTool {
|
||||
@@ -105,6 +106,7 @@ function FileSelectorSyncWrapper({
|
||||
onChange,
|
||||
uiComponent,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
@@ -112,6 +114,7 @@ function FileSelectorSyncWrapper({
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
@@ -128,6 +131,7 @@ function FileSelectorSyncWrapper({
|
||||
placeholder: uiComponent.placeholder,
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
@@ -398,6 +402,7 @@ export function ToolInput({
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
disabled = false,
|
||||
allowExpandInPreview,
|
||||
}: ToolInputProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -775,8 +780,19 @@ export function ToolInput({
|
||||
)
|
||||
}
|
||||
|
||||
// Local expansion overrides for preview/diff mode
|
||||
const [previewExpanded, setPreviewExpanded] = useState<Record<number, boolean>>({})
|
||||
|
||||
const toggleToolExpansion = (toolIndex: number) => {
|
||||
if (isPreview || disabled) return
|
||||
if ((isPreview && !allowExpandInPreview) || disabled) return
|
||||
|
||||
if (isPreview) {
|
||||
setPreviewExpanded((prev) => ({
|
||||
...prev,
|
||||
[toolIndex]: !(prev[toolIndex] ?? !!selectedTools[toolIndex]?.isExpanded),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
setStoreValue(
|
||||
selectedTools.map((tool, index) =>
|
||||
@@ -929,7 +945,8 @@ export function ToolInput({
|
||||
param: ToolParameterConfig,
|
||||
value: string,
|
||||
onChange: (value: string) => void,
|
||||
toolIndex?: number
|
||||
toolIndex?: number,
|
||||
currentToolParams?: Record<string, string>
|
||||
) => {
|
||||
// Create unique blockId for tool parameters to avoid conflicts with main block
|
||||
const uniqueBlockId = toolIndex !== undefined ? `${blockId}-tool-${toolIndex}` : blockId
|
||||
@@ -1076,6 +1093,7 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1363,6 +1381,9 @@ export function ToolInput({
|
||||
const oauthConfig = !isCustomTool ? getToolOAuthConfig(currentToolId) : null
|
||||
|
||||
// Tools are always expandable so users can access the interface
|
||||
const isExpandedForDisplay = isPreview
|
||||
? (previewExpanded[toolIndex] ?? !!tool.isExpanded)
|
||||
: !!tool.isExpanded
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1458,29 +1479,27 @@ export function ToolInput({
|
||||
</span>
|
||||
<span
|
||||
className={`font-medium text-xs ${
|
||||
tool.usageControl === 'force'
|
||||
? 'block text-muted-foreground'
|
||||
: 'hidden'
|
||||
tool.usageControl === 'force' ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
Force
|
||||
</span>
|
||||
<span
|
||||
className={`font-medium text-xs ${
|
||||
tool.usageControl === 'none'
|
||||
? 'block text-muted-foreground'
|
||||
: 'hidden'
|
||||
tool.usageControl === 'none' ? 'block' : 'hidden'
|
||||
}`}
|
||||
>
|
||||
Deny
|
||||
None
|
||||
</span>
|
||||
</Toggle>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='bottom' className='max-w-[240px] p-2'>
|
||||
<p className='text-xs'>
|
||||
<TooltipContent className='max-w-[280px] p-2' side='top'>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Control how the model uses this tool in its response.
|
||||
{tool.usageControl === 'auto' && (
|
||||
<span>
|
||||
<span className='font-medium'>Auto:</span> Let the agent decide
|
||||
{' '}
|
||||
<span className='font-medium'>Auto:</span> Let the model decide
|
||||
when to use the tool
|
||||
</span>
|
||||
)}
|
||||
@@ -1511,7 +1530,7 @@ export function ToolInput({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCustomTool && tool.isExpanded && (
|
||||
{!isCustomTool && isExpandedForDisplay && (
|
||||
<div className='space-y-3 overflow-visible p-3'>
|
||||
{/* Operation dropdown for tools with multiple operations */}
|
||||
{(() => {
|
||||
@@ -1660,7 +1679,8 @@ export function ToolInput({
|
||||
param,
|
||||
tool.params[param.id] || '',
|
||||
(value) => handleParamChange(toolIndex, param.id, value),
|
||||
toolIndex
|
||||
toolIndex,
|
||||
tool.params
|
||||
)
|
||||
) : (
|
||||
<ShortInput
|
||||
|
||||
@@ -44,6 +44,7 @@ interface SubBlockProps {
|
||||
subBlockValues?: Record<string, any>
|
||||
disabled?: boolean
|
||||
fieldDiffStatus?: 'changed' | 'unchanged'
|
||||
allowExpandInPreview?: boolean
|
||||
}
|
||||
|
||||
export function SubBlock({
|
||||
@@ -54,6 +55,7 @@ export function SubBlock({
|
||||
subBlockValues,
|
||||
disabled = false,
|
||||
fieldDiffStatus,
|
||||
allowExpandInPreview,
|
||||
}: SubBlockProps) {
|
||||
const [isValidJson, setIsValidJson] = useState(true)
|
||||
|
||||
@@ -211,7 +213,8 @@ export function SubBlock({
|
||||
subBlockId={config.id}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
disabled={isDisabled}
|
||||
disabled={allowExpandInPreview ? false : isDisabled}
|
||||
allowExpandInPreview={allowExpandInPreview}
|
||||
/>
|
||||
)
|
||||
case 'checkbox-list':
|
||||
@@ -355,6 +358,7 @@ export function SubBlock({
|
||||
disabled={isDisabled}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
previewContextValues={subBlockValues}
|
||||
/>
|
||||
)
|
||||
case 'project-selector':
|
||||
|
||||
@@ -151,6 +151,24 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false)
|
||||
const blockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false)
|
||||
|
||||
// Local UI state for diff mode controls
|
||||
const [diffIsWide, setDiffIsWide] = useState<boolean>(isWide)
|
||||
const [diffAdvancedMode, setDiffAdvancedMode] = useState<boolean>(blockAdvancedMode)
|
||||
const [diffTriggerMode, setDiffTriggerMode] = useState<boolean>(blockTriggerMode)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkflow.isDiffMode) {
|
||||
setDiffIsWide(isWide)
|
||||
setDiffAdvancedMode(blockAdvancedMode)
|
||||
setDiffTriggerMode(blockTriggerMode)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentWorkflow.isDiffMode, id])
|
||||
|
||||
const displayIsWide = currentWorkflow.isDiffMode ? diffIsWide : isWide
|
||||
const displayAdvancedMode = currentWorkflow.isDiffMode ? diffAdvancedMode : blockAdvancedMode
|
||||
const displayTriggerMode = currentWorkflow.isDiffMode ? diffTriggerMode : blockTriggerMode
|
||||
|
||||
// Collaborative workflow actions
|
||||
const {
|
||||
collaborativeUpdateBlockName,
|
||||
@@ -414,6 +432,8 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
|
||||
const isAdvancedMode = useWorkflowStore.getState().blocks[blockId]?.advancedMode ?? false
|
||||
const isTriggerMode = useWorkflowStore.getState().blocks[blockId]?.triggerMode ?? false
|
||||
const effectiveAdvanced = currentWorkflow.isDiffMode ? displayAdvancedMode : isAdvancedMode
|
||||
const effectiveTrigger = currentWorkflow.isDiffMode ? displayTriggerMode : isTriggerMode
|
||||
|
||||
// Filter visible blocks and those that meet their conditions
|
||||
const visibleSubBlocks = subBlocks.filter((block) => {
|
||||
@@ -423,18 +443,18 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
if (block.type === ('trigger-config' as SubBlockType)) {
|
||||
// Show trigger-config blocks when in trigger mode OR for pure trigger blocks
|
||||
const isPureTriggerBlock = config?.triggers?.enabled && config.category === 'triggers'
|
||||
return isTriggerMode || isPureTriggerBlock
|
||||
return effectiveTrigger || isPureTriggerBlock
|
||||
}
|
||||
|
||||
if (isTriggerMode && block.type !== ('trigger-config' as SubBlockType)) {
|
||||
if (effectiveTrigger && block.type !== ('trigger-config' as SubBlockType)) {
|
||||
// In trigger mode, hide all non-trigger-config blocks
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter by mode if specified
|
||||
if (block.mode) {
|
||||
if (block.mode === 'basic' && isAdvancedMode) return false
|
||||
if (block.mode === 'advanced' && !isAdvancedMode) return false
|
||||
if (block.mode === 'basic' && effectiveAdvanced) return false
|
||||
if (block.mode === 'advanced' && !effectiveAdvanced) return false
|
||||
}
|
||||
|
||||
// If there's no condition, the block should be shown
|
||||
@@ -562,7 +582,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
className={cn(
|
||||
'relative cursor-default select-none shadow-md',
|
||||
'transition-block-bg transition-ring',
|
||||
isWide ? 'w-[480px]' : 'w-[320px]',
|
||||
displayIsWide ? 'w-[480px]' : 'w-[320px]',
|
||||
!isEnabled && 'shadow-sm',
|
||||
isActive && 'animate-pulse-ring ring-2 ring-blue-500',
|
||||
isPending && 'ring-2 ring-amber-500',
|
||||
@@ -658,7 +678,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
onClick={handleNameClick}
|
||||
title={name}
|
||||
style={{
|
||||
maxWidth: !isEnabled ? (isWide ? '200px' : '140px') : '180px',
|
||||
maxWidth: !isEnabled ? (displayIsWide ? '200px' : '140px') : '180px',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
@@ -758,26 +778,30 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
if (userPermissions.canEdit) {
|
||||
if (currentWorkflow.isDiffMode) {
|
||||
setDiffAdvancedMode((prev) => !prev)
|
||||
} else if (userPermissions.canEdit) {
|
||||
collaborativeToggleBlockAdvancedMode(id)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'h-7 p-1 text-gray-500',
|
||||
blockAdvancedMode && 'text-[var(--brand-primary-hex)]',
|
||||
!userPermissions.canEdit && 'cursor-not-allowed opacity-50'
|
||||
displayAdvancedMode && 'text-[var(--brand-primary-hex)]',
|
||||
!userPermissions.canEdit &&
|
||||
!currentWorkflow.isDiffMode &&
|
||||
'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
disabled={!userPermissions.canEdit}
|
||||
disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode}
|
||||
>
|
||||
<Code className='h-5 w-5' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>
|
||||
{!userPermissions.canEdit
|
||||
{!userPermissions.canEdit && !currentWorkflow.isDiffMode
|
||||
? userPermissions.isOfflineMode
|
||||
? 'Connection lost - please refresh'
|
||||
: 'Read-only mode'
|
||||
: blockAdvancedMode
|
||||
: displayAdvancedMode
|
||||
? 'Switch to Basic Mode'
|
||||
: 'Switch to Advanced Mode'}
|
||||
</TooltipContent>
|
||||
@@ -791,27 +815,31 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
if (userPermissions.canEdit) {
|
||||
if (currentWorkflow.isDiffMode) {
|
||||
setDiffTriggerMode((prev) => !prev)
|
||||
} else if (userPermissions.canEdit) {
|
||||
// Toggle trigger mode using collaborative function
|
||||
collaborativeToggleBlockTriggerMode(id)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'h-7 p-1 text-gray-500',
|
||||
blockTriggerMode && 'text-[#22C55E]',
|
||||
!userPermissions.canEdit && 'cursor-not-allowed opacity-50'
|
||||
displayTriggerMode && 'text-[#22C55E]',
|
||||
!userPermissions.canEdit &&
|
||||
!currentWorkflow.isDiffMode &&
|
||||
'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
disabled={!userPermissions.canEdit}
|
||||
disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode}
|
||||
>
|
||||
<Zap className='h-5 w-5' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>
|
||||
{!userPermissions.canEdit
|
||||
{!userPermissions.canEdit && !currentWorkflow.isDiffMode
|
||||
? userPermissions.isOfflineMode
|
||||
? 'Connection lost - please refresh'
|
||||
: 'Read-only mode'
|
||||
: blockTriggerMode
|
||||
: displayTriggerMode
|
||||
? 'Switch to Action Mode'
|
||||
: 'Switch to Trigger Mode'}
|
||||
</TooltipContent>
|
||||
@@ -892,17 +920,21 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
if (userPermissions.canEdit) {
|
||||
if (currentWorkflow.isDiffMode) {
|
||||
setDiffIsWide((prev) => !prev)
|
||||
} else if (userPermissions.canEdit) {
|
||||
collaborativeToggleBlockWide(id)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'h-7 p-1 text-gray-500',
|
||||
!userPermissions.canEdit && 'cursor-not-allowed opacity-50'
|
||||
!userPermissions.canEdit &&
|
||||
!currentWorkflow.isDiffMode &&
|
||||
'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
disabled={!userPermissions.canEdit}
|
||||
disabled={!userPermissions.canEdit && !currentWorkflow.isDiffMode}
|
||||
>
|
||||
{isWide ? (
|
||||
{displayIsWide ? (
|
||||
<RectangleHorizontal className='h-5 w-5' />
|
||||
) : (
|
||||
<RectangleVertical className='h-5 w-5' />
|
||||
@@ -910,11 +942,11 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>
|
||||
{!userPermissions.canEdit
|
||||
{!userPermissions.canEdit && !currentWorkflow.isDiffMode
|
||||
? userPermissions.isOfflineMode
|
||||
? 'Connection lost - please refresh'
|
||||
: 'Read-only mode'
|
||||
: isWide
|
||||
: displayIsWide
|
||||
? 'Narrow Block'
|
||||
: 'Expand Block'}
|
||||
</TooltipContent>
|
||||
@@ -942,8 +974,13 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
blockId={id}
|
||||
config={subBlock}
|
||||
isConnecting={isConnecting}
|
||||
isPreview={data.isPreview}
|
||||
subBlockValues={data.subBlockValues}
|
||||
isPreview={data.isPreview || currentWorkflow.isDiffMode}
|
||||
subBlockValues={
|
||||
data.subBlockValues ||
|
||||
(currentWorkflow.isDiffMode && currentBlock
|
||||
? (currentBlock as any).subBlocks
|
||||
: undefined)
|
||||
}
|
||||
disabled={!userPermissions.canEdit}
|
||||
fieldDiffStatus={
|
||||
fieldDiff?.changed_fields?.includes(subBlock.id)
|
||||
@@ -952,6 +989,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
? 'unchanged'
|
||||
: undefined
|
||||
}
|
||||
allowExpandInPreview={currentWorkflow.isDiffMode}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -202,9 +202,6 @@ export function Copilot() {
|
||||
<div className='flex items-center justify-between gap-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='rounded bg-muted/50 px-2 py-1 font-mono text-sm'>{value}</div>
|
||||
<p className='mt-1 text-muted-foreground text-xs'>
|
||||
Key ID: <span className='font-mono'>{k.id}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -402,11 +402,11 @@ export const SERVER_TOOL_METADATA: Record<ServerToolId, ToolMetadata> = {
|
||||
id: SERVER_TOOL_IDS.GET_OAUTH_CREDENTIALS,
|
||||
displayConfig: {
|
||||
states: {
|
||||
executing: { displayName: 'Retrieving OAuth credentials', icon: 'spinner' },
|
||||
success: { displayName: 'Retrieved OAuth credentials', icon: 'key' },
|
||||
rejected: { displayName: 'Skipped retrieving OAuth credentials', icon: 'skip' },
|
||||
errored: { displayName: 'Failed to retrieve OAuth credentials', icon: 'error' },
|
||||
aborted: { displayName: 'Retrieving OAuth credentials aborted', icon: 'x' },
|
||||
executing: { displayName: 'Retrieving login IDs', icon: 'spinner' },
|
||||
success: { displayName: 'Retrieved login IDs', icon: 'key' },
|
||||
rejected: { displayName: 'Skipped retrieving login IDs', icon: 'skip' },
|
||||
errored: { displayName: 'Failed to retrieve login IDs', icon: 'error' },
|
||||
aborted: { displayName: 'Retrieving login IDs aborted', icon: 'x' },
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { db } from '@/db'
|
||||
import { account, user } from '@/db/schema'
|
||||
import { BaseCopilotTool } from '../base'
|
||||
@@ -15,6 +16,7 @@ interface OAuthCredentialItem {
|
||||
provider: string
|
||||
lastUsed: string
|
||||
isDefault: boolean
|
||||
accessToken: string | null
|
||||
}
|
||||
|
||||
interface GetOAuthCredentialsResult {
|
||||
@@ -55,6 +57,9 @@ class GetOAuthCredentialsTool extends BaseCopilotTool<
|
||||
|
||||
const credentials: OAuthCredentialItem[] = []
|
||||
|
||||
// Short request id for log correlation
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
for (const acc of accounts) {
|
||||
const providerId = acc.providerId
|
||||
const [baseProvider, featureType = 'default'] = providerId.split('-')
|
||||
@@ -90,12 +95,26 @@ class GetOAuthCredentialsTool extends BaseCopilotTool<
|
||||
displayName = `${acc.accountId} (${baseProvider})`
|
||||
}
|
||||
|
||||
// Ensure we return a valid access token, refreshing if needed
|
||||
let accessToken: string | null = acc.accessToken ?? null
|
||||
try {
|
||||
const { accessToken: refreshedToken } = await refreshTokenIfNeeded(
|
||||
requestId,
|
||||
acc as any,
|
||||
acc.id
|
||||
)
|
||||
accessToken = refreshedToken || accessToken
|
||||
} catch (_error) {
|
||||
// If refresh fails, we still return whatever we had (may be null)
|
||||
}
|
||||
|
||||
credentials.push({
|
||||
id: acc.id,
|
||||
name: displayName,
|
||||
provider: providerId,
|
||||
lastUsed: acc.updatedAt.toISOString(),
|
||||
isDefault: featureType === 'default',
|
||||
accessToken,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1814,7 +1814,7 @@ async function* parseSSEStream(
|
||||
const COPILOT_AUTH_REQUIRED_MESSAGE =
|
||||
'*Authorization failed. An API key must be configured in order to use the copilot. You can configure an API key at [sim.ai](https://sim.ai).*'
|
||||
const COPILOT_USAGE_EXCEEDED_MESSAGE =
|
||||
'*Usage limit exceeded, please upgrade your plan at [sim.ai](https://sim.ai) to continue using the copilot*'
|
||||
'*Usage limit exceeded, please upgrade your plan or top up credits at [sim.ai](https://sim.ai) to continue using the copilot*'
|
||||
|
||||
/**
|
||||
* Copilot store using the new unified API
|
||||
|
||||
@@ -2,6 +2,7 @@ import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { type DiffAnalysis, WorkflowDiffEngine } from '@/lib/workflows/diff'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { useWorkflowRegistry } from '../workflows/registry/store'
|
||||
import { useSubBlockStore } from '../workflows/subblock/store'
|
||||
import { useWorkflowStore } from '../workflows/workflow/store'
|
||||
@@ -47,6 +48,8 @@ interface WorkflowDiffState {
|
||||
source: string
|
||||
timestamp: number
|
||||
} | null
|
||||
// Store validation error when proposed diff is invalid for the canvas
|
||||
diffError?: string | null
|
||||
// PERFORMANCE OPTIMIZATION: Cache frequently accessed computed values
|
||||
_cachedDisplayState?: WorkflowState
|
||||
_lastDisplayStateHash?: string
|
||||
@@ -105,6 +108,7 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
diffWorkflow: null,
|
||||
diffAnalysis: null,
|
||||
diffMetadata: null,
|
||||
diffError: null,
|
||||
_cachedDisplayState: undefined,
|
||||
_lastDisplayStateHash: undefined,
|
||||
|
||||
@@ -112,7 +116,7 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
|
||||
setProposedChanges: async (yamlContent: string, diffAnalysis?: DiffAnalysis) => {
|
||||
// PERFORMANCE OPTIMIZATION: Immediate state update to prevent UI flicker
|
||||
batchedUpdate({ isDiffReady: false })
|
||||
batchedUpdate({ isDiffReady: false, diffError: null })
|
||||
|
||||
// Clear any existing diff state to ensure a fresh start
|
||||
diffEngine.clearDiff()
|
||||
@@ -120,6 +124,28 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
const result = await diffEngine.createDiffFromYaml(yamlContent, diffAnalysis)
|
||||
|
||||
if (result.success && result.diff) {
|
||||
// Validate proposed workflow using serializer round-trip to catch canvas-breaking issues
|
||||
try {
|
||||
const proposed = result.diff.proposedState
|
||||
const serializer = new Serializer()
|
||||
const serialized = serializer.serializeWorkflow(
|
||||
proposed.blocks,
|
||||
proposed.edges,
|
||||
proposed.loops,
|
||||
proposed.parallels,
|
||||
false // do not enforce user-only required params at diff time
|
||||
)
|
||||
// Ensure we can deserialize back without errors
|
||||
serializer.deserializeWorkflow(serialized)
|
||||
} catch (e: any) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : 'Invalid workflow in proposed changes'
|
||||
logger.error('[DiffStore] Diff validation failed:', { message, error: e })
|
||||
// Do not mark ready; store error and keep diff hidden
|
||||
batchedUpdate({ isDiffReady: false, diffError: message, isShowingDiff: false })
|
||||
return
|
||||
}
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Log diff analysis efficiently
|
||||
if (result.diff.diffAnalysis) {
|
||||
const analysis = result.diff.diffAnalysis
|
||||
@@ -138,6 +164,7 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
diffWorkflow: result.diff.proposedState,
|
||||
diffAnalysis: result.diff.diffAnalysis || null,
|
||||
diffMetadata: result.diff.metadata,
|
||||
diffError: null,
|
||||
_cachedDisplayState: undefined, // Clear cache
|
||||
_lastDisplayStateHash: undefined,
|
||||
})
|
||||
@@ -145,7 +172,10 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
logger.info('Diff created successfully')
|
||||
} else {
|
||||
logger.error('Failed to create diff:', result.errors)
|
||||
batchedUpdate({ isDiffReady: false })
|
||||
batchedUpdate({
|
||||
isDiffReady: false,
|
||||
diffError: result.errors?.join(', ') || 'Failed to create diff',
|
||||
})
|
||||
throw new Error(result.errors?.join(', ') || 'Failed to create diff')
|
||||
}
|
||||
},
|
||||
@@ -154,11 +184,31 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
logger.info('Merging proposed changes via YAML')
|
||||
|
||||
// First, set isDiffReady to false to prevent premature rendering
|
||||
batchedUpdate({ isDiffReady: false })
|
||||
batchedUpdate({ isDiffReady: false, diffError: null })
|
||||
|
||||
const result = await diffEngine.mergeDiffFromYaml(yamlContent, diffAnalysis)
|
||||
|
||||
if (result.success && result.diff) {
|
||||
// Validate proposed workflow using serializer round-trip to catch canvas-breaking issues
|
||||
try {
|
||||
const proposed = result.diff.proposedState
|
||||
const serializer = new Serializer()
|
||||
const serialized = serializer.serializeWorkflow(
|
||||
proposed.blocks,
|
||||
proposed.edges,
|
||||
proposed.loops,
|
||||
proposed.parallels,
|
||||
false
|
||||
)
|
||||
serializer.deserializeWorkflow(serialized)
|
||||
} catch (e: any) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : 'Invalid workflow in proposed changes'
|
||||
logger.error('[DiffStore] Diff validation failed on merge:', { message, error: e })
|
||||
batchedUpdate({ isDiffReady: false, diffError: message, isShowingDiff: false })
|
||||
return
|
||||
}
|
||||
|
||||
// Set all state at once, with isDiffReady true
|
||||
batchedUpdate({
|
||||
isShowingDiff: true,
|
||||
@@ -166,12 +216,16 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
diffWorkflow: result.diff.proposedState,
|
||||
diffAnalysis: result.diff.diffAnalysis || null,
|
||||
diffMetadata: result.diff.metadata,
|
||||
diffError: null,
|
||||
})
|
||||
logger.info('Diff merged successfully')
|
||||
} else {
|
||||
logger.error('Failed to merge diff:', result.errors)
|
||||
// Reset isDiffReady on failure
|
||||
batchedUpdate({ isDiffReady: false })
|
||||
batchedUpdate({
|
||||
isDiffReady: false,
|
||||
diffError: result.errors?.join(', ') || 'Failed to merge diff',
|
||||
})
|
||||
throw new Error(result.errors?.join(', ') || 'Failed to merge diff')
|
||||
}
|
||||
},
|
||||
@@ -185,6 +239,7 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
diffWorkflow: null,
|
||||
diffAnalysis: null,
|
||||
diffMetadata: null,
|
||||
diffError: null,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -248,48 +303,49 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
||||
|
||||
logger.info('Successfully applied diff workflow to main store')
|
||||
|
||||
// Persist to database
|
||||
try {
|
||||
logger.info('Persisting accepted diff changes to database')
|
||||
|
||||
const response = await fetch(`/api/workflows/${activeWorkflowId}/state`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...cleanState,
|
||||
lastSaved: Date.now(),
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
logger.error('Failed to persist accepted diff to database:', errorData)
|
||||
throw new Error(errorData.error || `Failed to save: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
logger.info('Successfully persisted accepted diff to database', {
|
||||
blocksCount: result.blocksCount,
|
||||
edgesCount: result.edgesCount,
|
||||
})
|
||||
} catch (persistError) {
|
||||
logger.error('Failed to persist accepted diff to database:', persistError)
|
||||
// Don't throw here - the store is already updated, so the UI is correct
|
||||
logger.warn('Diff was applied to local stores but not persisted to database')
|
||||
}
|
||||
|
||||
// Clear the diff
|
||||
// Optimistically clear the diff immediately so UI updates instantly
|
||||
get().clearDiff()
|
||||
|
||||
// Update copilot tool call state to 'accepted'
|
||||
try {
|
||||
const { useCopilotStore } = await import('@/stores/copilot/store')
|
||||
useCopilotStore.getState().updatePreviewToolCallState('accepted')
|
||||
} catch (error) {
|
||||
logger.warn('Failed to update copilot tool call state after accept:', error)
|
||||
}
|
||||
// Fire-and-forget: persist to database and update copilot state in the background
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
logger.info('Persisting accepted diff changes to database')
|
||||
|
||||
const response = await fetch(`/api/workflows/${activeWorkflowId}/state`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...cleanState,
|
||||
lastSaved: Date.now(),
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to persist accepted diff to database:', errorData)
|
||||
} else {
|
||||
const result = await response.json().catch(() => ({}))
|
||||
logger.info('Successfully persisted accepted diff to database', {
|
||||
blocksCount: (result as any)?.blocksCount,
|
||||
edgesCount: (result as any)?.edgesCount,
|
||||
})
|
||||
}
|
||||
} catch (persistError) {
|
||||
logger.error('Failed to persist accepted diff to database:', persistError)
|
||||
logger.warn('Diff was applied to local stores but not persisted to database')
|
||||
}
|
||||
|
||||
// Update copilot tool call state to 'accepted'
|
||||
try {
|
||||
const { useCopilotStore } = await import('@/stores/copilot/store')
|
||||
useCopilotStore.getState().updatePreviewToolCallState('accepted')
|
||||
} catch (error) {
|
||||
logger.warn('Failed to update copilot tool call state after accept:', error)
|
||||
}
|
||||
})()
|
||||
} catch (error) {
|
||||
logger.error('Failed to accept changes:', error)
|
||||
throw error
|
||||
|
||||
Reference in New Issue
Block a user