Broken checkpoint

This commit is contained in:
Siddharth Ganesan
2025-08-29 15:27:15 -07:00
parent 9c065a1c2a
commit 0c1ee239fe
6 changed files with 188 additions and 79 deletions

View File

@@ -16,6 +16,7 @@ import { TraceSpansDisplay } from '@/app/workspace/[workspaceId]/logs/components
import { formatDate } from '@/app/workspace/[workspaceId]/logs/utils/format-date'
import { formatCost } from '@/providers/utils'
import type { WorkflowLog } from '@/stores/logs/filters/types'
import { useRouter } from 'next/navigation'
interface LogSidebarProps {
log: WorkflowLog | null
@@ -529,15 +530,32 @@ export function Sidebar({
<h3 className='mb-1 font-medium text-muted-foreground text-xs'>
Workflow State
</h3>
<Button
variant='outline'
size='sm'
onClick={() => setIsFrozenCanvasOpen(true)}
className='w-full justify-start gap-2'
>
<Eye className='h-4 w-4' />
View Snapshot
</Button>
<div className='flex w-full gap-2'>
<Button
variant='outline'
size='sm'
onClick={() => setIsFrozenCanvasOpen(true)}
className='flex-1 justify-start gap-2'
>
<Eye className='h-4 w-4' />
View Snapshot
</Button>
<Button
variant='secondary'
size='sm'
onClick={() => {
try {
const router = useRouter()
const href = `/workspace/${encodeURIComponent(String(log.workflowId || ''))}/w/${encodeURIComponent(String(log.workflowId || ''))}`
router.push(href)
} catch {}
}}
className='flex-1 justify-start gap-2'
>
<Eye className='h-4 w-4' />
Open Live Debug
</Button>
</div>
<p className='mt-1 text-muted-foreground text-xs'>
See the exact workflow state and block inputs/outputs at execution time
</p>

View File

@@ -43,6 +43,7 @@ import {
useKeyboardShortcuts,
} from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts'
import { useExecutionStore } from '@/stores/execution/store'
import { useDebugCanvasStore } from '@/stores/execution/debug-canvas/store'
import { useFolderStore } from '@/stores/folders/store'
import { usePanelStore } from '@/stores/panel/store'
import { useGeneralStore } from '@/stores/settings/general/store'
@@ -817,6 +818,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
if (isDebugging) {
// Stop debugging
try { useDebugCanvasStore.getState().clear() } catch {}
handleCancelDebug()
} else {
// Check if there are executable blocks before starting debug mode
@@ -851,6 +853,8 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
if (starterId) {
execStore.setActiveBlocks(new Set([starterId]))
}
// Ensure debug canvas starts in a clean state
try { useDebugCanvasStore.getState().clear() } catch {}
}
}
}, [

View File

@@ -38,6 +38,8 @@ import { getTool } from '@/tools/utils'
import { getTrigger, getTriggersByProvider } from '@/triggers'
import { useParams } from 'next/navigation'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { useDebugCanvasStore } from '@/stores/execution/debug-canvas/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
// Token render cache (LRU-style)
const TOKEN_CACHE_MAX = 500
@@ -1789,6 +1791,33 @@ export function DebugPanel() {
}
}, [workspaceId, workflowId])
// Load selected execution's workflow into debug canvas
useEffect(() => {
if (!selectedExecutionKey) return
const selected = executions.find((e) => e.id === selectedExecutionKey)
const execId = selected?.executionId
if (!execId) return
let canceled = false
const load = async () => {
try {
const response = await fetch(`/api/logs/${encodeURIComponent(execId)}/frozen-canvas`)
const json = await response.json()
if (canceled) return
const state = json?.data?.workflowState as WorkflowState | undefined
if (state) {
useDebugCanvasStore.getState().activate(state)
}
} catch (err) {
// Silently ignore load errors
}
}
load()
return () => {
canceled = true
}
}, [selectedExecutionKey, executions])
if (!isDebugging) {
return (
<div className='flex h-full flex-col items-center justify-center px-6'>

View File

@@ -4,36 +4,38 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
import { useDebugCanvasStore } from '@/stores/execution/debug-canvas/store'
/**
* Interface for the current workflow abstraction
*/
export interface CurrentWorkflow {
// Current workflow state properties
blocks: Record<string, BlockState>
edges: Edge[]
loops: Record<string, Loop>
parallels: Record<string, Parallel>
lastSaved?: number
isDeployed?: boolean
deployedAt?: Date
deploymentStatuses?: Record<string, DeploymentStatus>
needsRedeployment?: boolean
hasActiveWebhook?: boolean
// Current workflow state properties
blocks: Record<string, BlockState>
edges: Edge[]
loops: Record<string, Loop>
parallels: Record<string, Parallel>
lastSaved?: number
isDeployed?: boolean
deployedAt?: Date
deploymentStatuses?: Record<string, DeploymentStatus>
needsRedeployment?: boolean
hasActiveWebhook?: boolean
// Mode information
isDiffMode: boolean
isNormalMode: boolean
// Mode information
isDiffMode: boolean
isNormalMode: boolean
isDebugCanvasMode?: boolean
// Full workflow state (for cases that need the complete object)
workflowState: WorkflowState
// Full workflow state (for cases that need the complete object)
workflowState: WorkflowState
// Helper methods
getBlockById: (blockId: string) => BlockState | undefined
getBlockCount: () => number
getEdgeCount: () => number
hasBlocks: () => boolean
hasEdges: () => boolean
// Helper methods
getBlockById: (blockId: string) => BlockState | undefined
getBlockCount: () => number
getEdgeCount: () => number
hasBlocks: () => boolean
hasEdges: () => boolean
}
/**
@@ -41,48 +43,78 @@ export interface CurrentWorkflow {
* Automatically handles diff vs normal mode without exposing the complexity to consumers.
*/
export function useCurrentWorkflow(): CurrentWorkflow {
// Get normal workflow state
const normalWorkflow = useWorkflowStore((state) => state.getWorkflowState())
// Get normal workflow state
const normalWorkflow = useWorkflowStore((state) => state.getWorkflowState())
// Get diff state - now including isDiffReady
const { isShowingDiff, isDiffReady, diffWorkflow } = useWorkflowDiffStore()
// Get diff state - now including isDiffReady
const { isShowingDiff, isDiffReady, diffWorkflow } = useWorkflowDiffStore()
// Create the abstracted interface
const currentWorkflow = useMemo((): CurrentWorkflow => {
// Determine which workflow to use - only use diff if it's ready
const hasDiffBlocks =
!!diffWorkflow && Object.keys((diffWorkflow as any).blocks || {}).length > 0
const shouldUseDiff = isShowingDiff && isDiffReady && hasDiffBlocks
const activeWorkflow = shouldUseDiff ? diffWorkflow : normalWorkflow
// Get debug canvas override
const debugCanvas = useDebugCanvasStore((s) => ({ isActive: s.isActive, workflowState: s.workflowState }))
return {
// Current workflow state
blocks: activeWorkflow.blocks,
edges: activeWorkflow.edges,
loops: activeWorkflow.loops || {},
parallels: activeWorkflow.parallels || {},
lastSaved: activeWorkflow.lastSaved,
isDeployed: activeWorkflow.isDeployed,
deployedAt: activeWorkflow.deployedAt,
deploymentStatuses: activeWorkflow.deploymentStatuses,
needsRedeployment: activeWorkflow.needsRedeployment,
hasActiveWebhook: activeWorkflow.hasActiveWebhook,
// Create the abstracted interface
const currentWorkflow = useMemo((): CurrentWorkflow => {
// Prefer debug canvas if active
const hasDebugCanvas = !!debugCanvas.isActive && !!debugCanvas.workflowState
if (hasDebugCanvas) {
const activeWorkflow = debugCanvas.workflowState as WorkflowState
return {
blocks: activeWorkflow.blocks,
edges: activeWorkflow.edges,
loops: activeWorkflow.loops || {},
parallels: activeWorkflow.parallels || {},
lastSaved: activeWorkflow.lastSaved,
isDeployed: activeWorkflow.isDeployed,
deployedAt: activeWorkflow.deployedAt,
deploymentStatuses: activeWorkflow.deploymentStatuses,
needsRedeployment: activeWorkflow.needsRedeployment,
hasActiveWebhook: activeWorkflow.hasActiveWebhook,
isDiffMode: false,
isNormalMode: false,
isDebugCanvasMode: true,
workflowState: activeWorkflow,
getBlockById: (blockId: string) => activeWorkflow.blocks[blockId],
getBlockCount: () => Object.keys(activeWorkflow.blocks).length,
getEdgeCount: () => activeWorkflow.edges.length,
hasBlocks: () => Object.keys(activeWorkflow.blocks).length > 0,
hasEdges: () => activeWorkflow.edges.length > 0,
}
}
// Mode information - update to reflect ready state
isDiffMode: shouldUseDiff,
isNormalMode: !shouldUseDiff,
// Determine which workflow to use - only use diff if it's ready
const hasDiffBlocks = !!diffWorkflow && Object.keys((diffWorkflow as any).blocks || {}).length > 0
const shouldUseDiff = isShowingDiff && isDiffReady && hasDiffBlocks
const activeWorkflow = shouldUseDiff ? diffWorkflow : normalWorkflow
// Full workflow state (for cases that need the complete object)
workflowState: activeWorkflow,
return {
// Current workflow state
blocks: activeWorkflow.blocks,
edges: activeWorkflow.edges,
loops: activeWorkflow.loops || {},
parallels: activeWorkflow.parallels || {},
lastSaved: activeWorkflow.lastSaved,
isDeployed: activeWorkflow.isDeployed,
deployedAt: activeWorkflow.deployedAt,
deploymentStatuses: activeWorkflow.deploymentStatuses,
needsRedeployment: activeWorkflow.needsRedeployment,
hasActiveWebhook: activeWorkflow.hasActiveWebhook,
// Helper methods
getBlockById: (blockId: string) => activeWorkflow.blocks[blockId],
getBlockCount: () => Object.keys(activeWorkflow.blocks).length,
getEdgeCount: () => activeWorkflow.edges.length,
hasBlocks: () => Object.keys(activeWorkflow.blocks).length > 0,
hasEdges: () => activeWorkflow.edges.length > 0,
}
}, [normalWorkflow, isShowingDiff, isDiffReady, diffWorkflow])
// Mode information - update to reflect ready state
isDiffMode: shouldUseDiff,
isNormalMode: !shouldUseDiff,
isDebugCanvasMode: false,
return currentWorkflow
// Full workflow state (for cases that need the complete object)
workflowState: activeWorkflow,
// Helper methods
getBlockById: (blockId: string) => activeWorkflow.blocks[blockId],
getBlockCount: () => Object.keys(activeWorkflow.blocks).length,
getEdgeCount: () => activeWorkflow.edges.length,
hasBlocks: () => Object.keys(activeWorkflow.blocks).length > 0,
hasEdges: () => activeWorkflow.edges.length > 0,
}
}, [normalWorkflow, isShowingDiff, isDiffReady, diffWorkflow, debugCanvas.isActive, debugCanvas.workflowState])
return currentWorkflow
}

View File

@@ -1635,6 +1635,8 @@ const WorkflowContent = React.memo(() => {
)
}
const isReadOnly = currentWorkflow.isDebugCanvasMode === true ? true : !effectivePermissions.canEdit
return (
<div className='flex h-screen w-full flex-col overflow-hidden'>
<div className='relative h-full w-full flex-1 transition-all duration-200'>
@@ -1650,11 +1652,11 @@ const WorkflowContent = React.memo(() => {
edges={edgesWithSelection}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={effectivePermissions.canEdit ? onConnect : undefined}
onConnect={isReadOnly ? undefined : onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onDrop={effectivePermissions.canEdit ? onDrop : undefined}
onDragOver={effectivePermissions.canEdit ? onDragOver : undefined}
onDrop={isReadOnly ? undefined : onDrop}
onDragOver={isReadOnly ? undefined : onDragOver}
fitView
minZoom={0.1}
maxZoom={1.3}
@@ -1674,22 +1676,22 @@ const WorkflowContent = React.memo(() => {
onEdgeClick={onEdgeClick}
elementsSelectable={true}
selectNodesOnDrag={false}
nodesConnectable={effectivePermissions.canEdit}
nodesDraggable={effectivePermissions.canEdit}
nodesConnectable={!isReadOnly}
nodesDraggable={!isReadOnly}
draggable={false}
noWheelClassName='allow-scroll'
edgesFocusable={true}
edgesUpdatable={effectivePermissions.canEdit}
edgesUpdatable={!isReadOnly}
className='workflow-container h-full'
onNodeDrag={effectivePermissions.canEdit ? onNodeDrag : undefined}
onNodeDragStop={effectivePermissions.canEdit ? onNodeDragStop : undefined}
onNodeDragStart={effectivePermissions.canEdit ? onNodeDragStart : undefined}
onNodeDrag={isReadOnly ? undefined : onNodeDrag}
onNodeDragStop={isReadOnly ? undefined : onNodeDragStop}
onNodeDragStart={isReadOnly ? undefined : onNodeDragStart}
snapToGrid={false}
snapGrid={[20, 20]}
elevateEdgesOnSelect={true}
elevateNodesOnSelect={true}
autoPanOnConnect={effectivePermissions.canEdit}
autoPanOnNodeDrag={effectivePermissions.canEdit}
autoPanOnConnect={!isReadOnly}
autoPanOnNodeDrag={!isReadOnly}
>
<Background
color='hsl(var(--workflow-dots))'

View File

@@ -0,0 +1,24 @@
import { create } from 'zustand'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
interface DebugCanvasState {
isActive: boolean
workflowState: WorkflowState | null
}
interface DebugCanvasActions {
activate: (workflowState: WorkflowState) => void
deactivate: () => void
setWorkflowState: (workflowState: WorkflowState | null) => void
clear: () => void
}
export const useDebugCanvasStore = create<DebugCanvasState & DebugCanvasActions>()((set) => ({
isActive: false,
workflowState: null,
activate: (workflowState) => set({ isActive: true, workflowState }),
deactivate: () => set({ isActive: false, workflowState: null }),
setWorkflowState: (workflowState) => set({ workflowState }),
clear: () => set({ isActive: false, workflowState: null }),
}))