Skip to content

feat(debug): create debugger#1174

Merged
Sg312 merged 30 commits intostagingfrom
improvement/debug-mode
Aug 29, 2025
Merged

feat(debug): create debugger#1174
Sg312 merged 30 commits intostagingfrom
improvement/debug-mode

Conversation

@Sg312
Copy link
Contributor

@Sg312 Sg312 commented Aug 29, 2025

Summary

Adds a proper debugger

Fixes #(issue)

Type of Change

  • New feature

Testing

Manual testing

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link

vercel bot commented Aug 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
sim Ready Ready Preview Comment Aug 29, 2025 1:19am
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
docs Skipped Skipped Aug 29, 2025 1:19am

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Summary

This PR implements a comprehensive debugging system for workflow execution in the Sim platform. The changes introduce a dedicated debug panel with step-by-step execution capabilities, variable inspection, and visual feedback for workflow debugging.

The core architectural changes include:

State Management Extensions: The execution store is extended with new debugging-specific state properties (panelFocusedBlockId, executingBlockIds, breakpointId) to track which blocks are focused in the debug panel, currently executing, and serve as breakpoints. These follow the established Zustand store pattern with proper state isolation.

Debug Panel Integration: A new 'debug' tab is added to the panel system, with a comprehensive 2000+ line debug component that handles complex scenarios including parallel execution (via virtual block IDs), variable scoping, environment variable filtering, and real-time execution tracking. The panel provides input/output visualization, clickable token navigation, and multi-tab interfaces for different aspects of debugging.

Execution Flow Changes: The executor now marks blocks with initial states as executed: false instead of executed: true, enabling proper debug stepping through the entire workflow including pre-existing states. Debug mode disables streaming execution to maintain synchronous, controllable execution flow essential for debugging.

Visual Feedback System: Workflow blocks now display comprehensive visual indicators including error states (highest priority), execution states (pulsing rings), focused states, and breakpoint indicators. The system handles both direct block execution and parallel execution scenarios with special virtual block ID logic.

Control Flow Reorganization: Debug controls are moved from the control bar to the dedicated debug panel, with the debug mode toggle now serving as a setup action that prepares the debugging environment without immediately executing. This provides a cleaner separation between high-level workflow actions and detailed debugging operations.

The implementation integrates deeply with the existing execution system while maintaining backward compatibility, leveraging the established store architecture and UI patterns throughout the codebase.

Confidence score: 3/5

  • This PR introduces significant complexity with potential edge cases in parallel execution and state management
  • Score reflects the substantial scope of changes across critical execution paths and complex debugging logic
  • Pay close attention to the debug panel component and execution state management files

9 files reviewed, 8 comments

Edit Code Review Bot Settings | Greptile

context.blockStates.set(blockId, {
output: output as NormalizedBlockOutput,
executed: true,
executed: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Changing executed: false enables debug stepping through blocks with initial states. Verify this doesn't break non-debug execution paths where these blocks should remain executed.

executor: Executor | null
debugContext: ExecutionContext | null
autoPanDisabled: boolean
panelFocusedBlockId?: string | null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider making panelFocusedBlockId non-optional since it's initialized to null - the optional modifier is redundant

Suggested change
panelFocusedBlockId?: string | null
panelFocusedBlockId: string | null

<div className='fixed top-[76px] right-4 z-20 flex h-9 w-[308px] items-center gap-1 rounded-[14px] border bg-card px-[2.5px] py-1 shadow-xs'>
<div
className='fixed top-[76px] right-4 z-20 flex h-9 items-center gap-1 rounded-[14px] border bg-card px-[2.5px] py-1 shadow-xs'
style={{ width: isDebugging ? '380px' : '308px' }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: dynamic width calculation could use CSS custom properties for maintainability

Comment on lines +573 to +834
// Compute accessible output variables for the focused block with tag-style references
const outputVariableEntries = useMemo(() => {
if (!focusedBlockId) return [] as Array<{ ref: string; value: any }>

const normalizeBlockName = (name: string) => (name || '').replace(/\s+/g, '').toLowerCase()
const getSubBlockValue = (blockId: string, property: string): any => {
return useSubBlockStore.getState().getValue(blockId, property)
}
const generateOutputPaths = (outputs: Record<string, any>, prefix = ''): string[] => {
const paths: string[] = []
for (const [key, value] of Object.entries(outputs || {})) {
const current = prefix ? `${prefix}.${key}` : key
if (typeof value === 'string') {
paths.push(current)
} else if (value && typeof value === 'object') {
if ('type' in value && typeof (value as any).type === 'string') {
paths.push(current)
if ((value as any).type === 'object' && (value as any).properties) {
paths.push(...generateOutputPaths((value as any).properties, current))
} else if ((value as any).type === 'array' && (value as any).items?.properties) {
paths.push(...generateOutputPaths((value as any).items.properties, current))
}
} else {
paths.push(...generateOutputPaths(value as Record<string, any>, current))
}
} else {
paths.push(current)
}
}
return paths
}

const getAccessiblePathsForBlock = (blockId: string): string[] => {
const blk = blockById.get(blockId)
if (!blk) return []
const cfg = getBlock(blk.type)
if (!cfg) return []

// Response format overrides
const responseFormatValue = getSubBlockValue(blockId, 'responseFormat')
const responseFormat = parseResponseFormatSafely(responseFormatValue, blockId)
if (responseFormat) {
const fields = extractFieldsFromSchema(responseFormat)
if (fields.length > 0) return fields.map((f: any) => f.name)
}

if (blk.type === 'evaluator') {
const metricsValue = getSubBlockValue(blockId, 'metrics')
if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) {
const valid = metricsValue.filter((m: { name?: string }) => m?.name)
return valid.map((m: { name: string }) => m.name.toLowerCase())
}
return generateOutputPaths(cfg.outputs || {})
}

if (blk.type === 'starter') {
const startWorkflowValue = getSubBlockValue(blockId, 'startWorkflow')
if (startWorkflowValue === 'chat') {
return ['input', 'conversationId', 'files']
}
const inputFormatValue = getSubBlockValue(blockId, 'inputFormat')
if (inputFormatValue && Array.isArray(inputFormatValue)) {
return inputFormatValue
.filter((f: { name?: string }) => f.name && f.name.trim() !== '')
.map((f: { name: string }) => f.name)
}
return []
}

if (blk.triggerMode && cfg.triggers?.enabled) {
const triggerId = cfg?.triggers?.available?.[0]
const firstTrigger = triggerId ? getTrigger(triggerId) : getTriggersByProvider(blk.type)[0]
if (firstTrigger?.outputs) {
return generateOutputPaths(firstTrigger.outputs)
}
}

const operationValue = getSubBlockValue(blockId, 'operation')
if (operationValue && cfg?.tools?.config?.tool) {
try {
const toolId = cfg.tools.config.tool({ operation: operationValue })
const toolConfig = toolId ? getTool(toolId) : null
if (toolConfig?.outputs) return generateOutputPaths(toolConfig.outputs)
} catch {}
}

return generateOutputPaths(cfg.outputs || {})
}

const edges = currentWorkflow.edges || []
const accessibleIds = new Set<string>(
BlockPathCalculator.findAllPathNodes(edges, focusedBlockId)
)

// Always allow referencing the starter block
if (starterId && starterId !== focusedBlockId) accessibleIds.add(starterId)

const entries: Array<{ ref: string; value: any }> = []

// Helper: collect executed outputs including virtual parallel iterations and loop/parallel context items
const collectExecutedOutputs = (baseId: string): Record<string, any>[] => {
const collected: Record<string, any>[] = []
const bs = debugContext?.blockStates
if (bs) {
const direct = bs.get(baseId)?.output
if (direct && typeof direct === 'object') collected.push(direct)
// Include virtual executions for parallels
try {
for (const [key, state] of bs.entries()) {
const mapping = debugContext?.parallelBlockMapping?.get(key as any)
if (mapping && mapping.originalBlockId === baseId && state?.output) {
collected.push(state.output as any)
}
}
} catch {}
}
return collected
}

// Add loop/parallel special variables if block is inside a loop or parallel
const addLoopParallelVariables = () => {
if (!debugContext) return

// Check if focused block is inside a loop
for (const [loopId, loop] of Object.entries(currentWorkflow.loops || {})) {
if ((loop as any).nodes?.includes(focusedBlockId)) {
// Add loop.item and loop.index references
const loopItem = debugContext.loopItems?.get(loopId)
const loopIndex = debugContext.loopIterations?.get(loopId)
const loopItems = debugContext.loopItems?.get(`${loopId}_items`)

if (loopItem !== undefined) {
entries.push({ ref: '<loop.item>', value: loopItem })
}
if (loopIndex !== undefined) {
entries.push({ ref: '<loop.index>', value: loopIndex })
}
if (loopItems !== undefined) {
entries.push({ ref: '<loop.items>', value: loopItems })
}

// Also add references for the loop block itself if it has executed
const loopBlock = blockById.get(loopId)
if (loopBlock) {
const loopName = normalizeBlockName(getDisplayName(loopBlock))
if (loopItem !== undefined) {
entries.push({ ref: `<${loopName}.item>`, value: loopItem })
}
if (loopIndex !== undefined) {
entries.push({ ref: `<${loopName}.index>`, value: loopIndex })
}
if (loopItems !== undefined) {
entries.push({ ref: `<${loopName}.items>`, value: loopItems })
}
}
}
}

// Check if focused block is inside a parallel
for (const [parallelId, parallel] of Object.entries(currentWorkflow.parallels || {})) {
if ((parallel as any).nodes?.includes(focusedBlockId)) {
// Check for virtual block execution to get iteration info
const parallelState = debugContext.parallelExecutions?.get(parallelId)
if (parallelState) {
// Get current iteration context
const currentVirtualId = debugContext.currentVirtualBlockId
if (currentVirtualId) {
const mapping = debugContext.parallelBlockMapping?.get(currentVirtualId)
if (mapping) {
const iterationIndex = mapping.iterationIndex
const parallelItems = debugContext.loopItems?.get(`${parallelId}_items`)
const parallelItem = parallelItems
? Array.isArray(parallelItems)
? parallelItems[iterationIndex]
: Object.values(parallelItems)[iterationIndex]
: undefined

if (parallelItem !== undefined) {
entries.push({ ref: '<parallel.item>', value: parallelItem })
}
entries.push({ ref: '<parallel.index>', value: iterationIndex })
if (parallelItems !== undefined) {
entries.push({ ref: '<parallel.items>', value: parallelItems })
}

// Also add references for the parallel block itself
const parallelBlock = blockById.get(parallelId)
if (parallelBlock) {
const parallelName = normalizeBlockName(getDisplayName(parallelBlock))
if (parallelItem !== undefined) {
entries.push({ ref: `<${parallelName}.item>`, value: parallelItem })
}
entries.push({ ref: `<${parallelName}.index>`, value: iterationIndex })
if (parallelItems !== undefined) {
entries.push({ ref: `<${parallelName}.items>`, value: parallelItems })
}
}
}
}
}
}
}
}

for (const id of accessibleIds) {
const blk = blockById.get(id)
if (!blk) continue

const allowedPathsSet = new Set<string>(getAccessiblePathsForBlock(id))
if (allowedPathsSet.size === 0) continue

const displayName = getDisplayName(blk)
const normalizedName = normalizeBlockName(displayName)

// Gather executed outputs (direct and virtual)
const executedOutputs = collectExecutedOutputs(id)

// Flatten helper over multiple outputs with last-wins per path
const pathToValue = new Map<string, any>()
const flatten = (obj: any, prefix = ''): Array<{ path: string; value: any }> => {
if (obj == null || typeof obj !== 'object') return []
const items: Array<{ path: string; value: any }> = []
for (const [k, v] of Object.entries(obj)) {
const current = prefix ? `${prefix}.${k}` : k
if (v && typeof v === 'object' && !Array.isArray(v)) {
if (allowedPathsSet.has(current)) items.push({ path: current, value: v })
items.push(...flatten(v, current))
} else {
if (allowedPathsSet.has(current)) items.push({ path: current, value: v })
}
}
return items
}

for (const out of executedOutputs) {
const pairs = flatten(out)
for (const { path, value } of pairs) {
pathToValue.set(path, value)
}
}

for (const [path, value] of pathToValue.entries()) {
entries.push({ ref: `<${normalizedName}.${path}>`, value })
}
}

// Add loop/parallel context variables
addLoopParallelVariables()

// Sort for stable UI (by ref)
entries.sort((a, b) => a.ref.localeCompare(b.ref))
return entries
}, [
focusedBlockId,
currentWorkflow.edges,
currentWorkflow.loops,
currentWorkflow.parallels,
starterId,
blockById,
executionVersion,
debugContext,
])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: outputVariableEntries memo is extremely large (261 lines) with multiple nested helper functions - this should be extracted into separate functions or a custom hook

Context Used: Context - If a switch statement is large and handles multiple cases, extract each case into separate functions for better maintainability. (link)

Comment on lines +1268 to +1276
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms))
let attempts = 0
while (attempts < 40) {
// ~2s max
const st = useExecutionStore.getState()
if (st.executor && st.debugContext && Array.isArray(st.pendingBlocks)) break
await wait(50)
attempts++
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: polling loop with hardcoded timing (50ms intervals, 40 attempts) could be unreliable - consider using a more robust async pattern or make timing configurable

Comment on lines +1330 to +1332
} catch (e) {
// Swallow to avoid double error surfaces in UI
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: silently swallowing all errors with empty catch block could hide important debugging information

className='rounded bg-blue-50 px-1.5 py-0.5 font-mono text-[11px] text-blue-700 transition-colors hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/30'
onClick={(e) => {
e.stopPropagation()
handleTokenClick(match.type as any, match.value, match.raw)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: type assertion 'as any' bypasses TypeScript safety - consider proper typing

Context Used: Context - Avoid using type assertions to 'any' in TypeScript. Instead, ensure proper type definitions are used to maintain type safety. (link)

openConsolePanel()
handleRunWorkflow(undefined, true) // Start in debug mode
// Activate debug session state so the panel is active
const execStore = useExecutionStore.getState()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Direct store access bypasses React state management. Consider using the existing execution hook methods instead of useExecutionStore.getState()

@Sg312 Sg312 merged commit 7c73f5f into staging Aug 29, 2025
5 checks passed
Sg312 added a commit that referenced this pull request Aug 29, 2025
Sg312 added a commit that referenced this pull request Aug 29, 2025
arenadeveloper02 pushed a commit to arenadeveloper02/p2-sim that referenced this pull request Sep 19, 2025
* Updates

* Updates

* Updates

* Checkpoint

* Checkpoint

* Checkpoitn

* Var improvements

* Fixes

* Execution status

* UI improvements

* Ui updates

* Fix

* Fix scoping

* Fix workflow vars

* Fix env vars

* Remove number styling

* Variable highlighting

* Updates

* Update

* Fix resume

* Stuff

* Breakpoint ui

* Ui

* Ui updates

* Loops and parallels

* HIde env vars

* Checkpoint

* Stuff

* Panel toggle

* Lint
arenadeveloper02 pushed a commit to arenadeveloper02/p2-sim that referenced this pull request Sep 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant