Compare commits

..

15 Commits

Author SHA1 Message Date
Waleed
f1ec5fe824 v0.5.104: memory improvements, nested subflows, careers page redirect, brandfetch, google meet 2026-03-03 23:45:29 -08:00
Waleed
efc1aeed70 fix(subflows): fix pointer events for nested subflow interaction (#3409)
* fix(subflows): fix pointer events for nested subflow interaction

* fix(subflows): use Tailwind class for pointer-events-none
2026-03-03 23:28:51 -08:00
Waleed
e07e3c34cc v0.5.103: memory util instrumentation, API docs, amplitude, google pagespeed insights, pagerduty 2026-03-01 23:27:02 -08:00
Waleed
0d2e6ff31d v0.5.102: new integrations, new tools, ci speedups, memory leak instrumentation 2026-02-28 12:48:10 -08:00
Waleed
4fd0989264 v0.5.101: circular dependency mitigation, confluence enhancements, google tasks and bigquery integrations, workflow lock 2026-02-26 15:04:53 -08:00
Waleed
67f8a687f6 v0.5.100: multiple credentials, 40% speedup, gong, attio, audit log improvements 2026-02-25 00:28:25 -08:00
Waleed
af592349d3 v0.5.99: local dev improvements, live workflow logs in terminal 2026-02-23 00:24:49 -08:00
Waleed
0d86ea01f0 v0.5.98: change detection improvements, rate limit and code execution fixes, removed retired models, hex integration 2026-02-21 18:07:40 -08:00
Waleed
115f04e989 v0.5.97: oidc discovery for copilot mcp 2026-02-21 02:06:25 -08:00
Waleed
34d92fae89 v0.5.96: sim oauth provider, slack ephemeral message tool and blockkit support 2026-02-20 18:22:20 -08:00
Waleed
67aa4bb332 v0.5.95: gemini 3.1 pro, cloudflare, dataverse, revenuecat, redis, upstash, algolia tools; isolated-vm robustness improvements, tables backend (#3271)
* feat(tools): advanced fields for youtube, vercel; added cloudflare and dataverse tools (#3257)

* refactor(vercel): mark optional fields as advanced mode

Move optional/power-user fields behind the advanced toggle:
- List Deployments: project filter, target, state
- Create Deployment: project ID override, redeploy from, target
- List Projects: search
- Create/Update Project: framework, build/output/install commands
- Env Vars: variable type
- Webhooks: project IDs filter
- Checks: path, details URL
- Team Members: role filter
- All operations: team ID scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(youtube): mark optional params as advanced mode

Hide pagination, sort order, and filter fields behind the advanced
toggle for a cleaner default UX across all YouTube operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* added advanced fields for vercel and youtube, added cloudflare and dataverse block

* addded desc for dataverse

* add more tools

* ack comment

* more

* ops

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tables): added tables (#2867)

* updates

* required

* trashy table viewer

* updates

* updates

* filtering ui

* updates

* updates

* updates

* one input mode

* format

* fix lints

* improved errors

* updates

* updates

* chages

* doc strings

* breaking down file

* update comments with ai

* updates

* comments

* changes

* revert

* updates

* dedupe

* updates

* updates

* updates

* refactoring

* renames & refactors

* refactoring

* updates

* undo

* update db

* wand

* updates

* fix comments

* fixes

* simplify comments

* u[dates

* renames

* better comments

* validation

* updates

* updates

* updates

* fix sorting

* fix appearnce

* updating prompt to make it user sort

* rm

* updates

* rename

* comments

* clean comments

* simplicifcaiton

* updates

* updates

* refactor

* reduced type confusion

* undo

* rename

* undo changes

* undo

* simplify

* updates

* updates

* revert

* updates

* db updates

* type fix

* fix

* fix error handling

* updates

* docs

* docs

* updates

* rename

* dedupe

* revert

* uncook

* updates

* fix

* fix

* fix

* fix

* prepare merge

* readd migrations

* add back missed code

* migrate enrichment logic to general abstraction

* address bugbot concerns

* adhere to size limits for tables

* remove conflicting migration

* add back migrations

* fix tables auth

* fix permissive auth

* fix lint

* reran migrations

* migrate to use tanstack query for all server state

* update table-selector

* update names

* added tables to permission groups, updated subblock types

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: waleed <walif6@gmail.com>

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running (#3259)

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running

* fixed ci tests failing

* fix(workflows): disallow duplicate workflow names at the same folder level (#3260)

* feat(tools): added redis, upstash, algolia, and revenuecat (#3261)

* feat(tools): added redis, upstash, algolia, and revenuecat

* ack comment

* feat(models): add gemini-3.1-pro-preview and update gemini-3-pro thinking levels (#3263)

* fix(audit-log): lazily resolve actor name/email when missing (#3262)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params (#3264)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params

Number() coercions in tools.config.tool ran at serialization time before
variable resolution, destroying dynamic references like <block.result.count>
by converting them to NaN/null. Moved all coercions to tools.config.params
which runs at execution time after variables are resolved.

Fixed in 15 blocks: exa, arxiv, sentry, incidentio, wikipedia, ahrefs,
posthog, elasticsearch, dropbox, hunter, lemlist, spotify, youtube, grafana,
parallel. Also added mode: 'advanced' to optional exa fields.

Closes #3258

* fix(blocks): address PR review — move remaining param mutations from tool() to params()

- Moved field mappings from tool() to params() in grafana, posthog,
  lemlist, spotify, dropbox (same dynamic reference bug)
- Fixed parallel.ts excerpts/full_content boolean logic
- Fixed parallel.ts search_queries empty case (must set undefined)
- Fixed elasticsearch.ts timeout not included when already ends with 's'
- Restored dropbox.ts tool() switch for proper default fallback

* fix(blocks): restore field renames to tool() for serialization-time validation

Field renames (e.g. personalApiKey→apiKey) must be in tool() because
validateRequiredFieldsBeforeExecution calls selectToolId()→tool() then
checks renamed field names on params. Only type coercions (Number(),
boolean) stay in params() to avoid destroying dynamic variable references.

* improvement(resolver): resovled empty sentinel to not pass through unexecuted valid refs to text inputs (#3266)

* fix(blocks): add required constraint for serviceDeskId in JSM block (#3268)

* fix(blocks): add required constraint for serviceDeskId in JSM block

* fix(blocks): rename custom field values to request field values in JSM create request

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* fix(tables): hide tables from sidebar and block registry (#3270)

* fix(tables): hide tables from sidebar and block registry

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* lint

* fix(trigger): update node version to align with main app (#3272)

* fix(build): fix corrupted sticky disk cache on blacksmith (#3273)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
2026-02-20 13:43:07 -08:00
Waleed
15ace5e63f v0.5.94: vercel integration, folder insertion, migrated tracking redirects to rewrites 2026-02-18 16:53:34 -08:00
Waleed
fdca73679d v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot keyboard shortcuts, audit logs 2026-02-18 12:10:05 -08:00
Waleed
da46a387c9 v0.5.92: shortlinks, copilot scrolling stickiness, pagination 2026-02-17 15:13:21 -08:00
Waleed
b7e377ec4b v0.5.91: docs i18n, turborepo upgrade 2026-02-16 00:36:05 -08:00
8 changed files with 30 additions and 400 deletions

View File

@@ -164,7 +164,7 @@ export const ActionBar = memo(
return (
<div
className={cn(
'-top-[46px] absolute right-0',
'-top-[46px] pointer-events-auto absolute right-0',
'flex flex-row items-center',
'opacity-0 transition-opacity duration-200 group-hover:opacity-100',
'gap-[5px] rounded-[10px] p-[5px]',

View File

@@ -31,12 +31,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
import {
restoreCursorAfterInsertion,
sanitizeForParsing,
validateJavaScript,
validatePython,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { restoreCursorAfterInsertion } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/utils'
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
@@ -171,7 +166,7 @@ interface CodeProps {
defaultCollapsed?: boolean
defaultValue?: string | number | boolean | Record<string, unknown> | Array<unknown>
showCopyButton?: boolean
onValidationChange?: (isValid: boolean, errorMessage?: string | null) => void
onValidationChange?: (isValid: boolean) => void
wandConfig: {
enabled: boolean
prompt: string
@@ -255,18 +250,6 @@ export const Code = memo(function Code({
}
}, [shouldValidateJson, trimmedCode])
const syntaxError = useMemo(() => {
if (effectiveLanguage === 'json' || !trimmedCode) return null
const sanitized = sanitizeForParsing(trimmedCode)
if (effectiveLanguage === 'javascript') {
return validateJavaScript(sanitized)
}
if (effectiveLanguage === 'python') {
return validatePython(sanitized)
}
return null
}, [effectiveLanguage, trimmedCode])
const gutterWidthPx = useMemo(() => {
const lineCount = code.split('\n').length
return calculateGutterWidth(lineCount)
@@ -358,21 +341,19 @@ export const Code = memo(function Code({
useEffect(() => {
if (!onValidationChange) return
const isValid = (!shouldValidateJson || isValidJson) && !syntaxError
const isValid = !shouldValidateJson || isValidJson
if (isValid) {
onValidationChange(true, null)
onValidationChange(true)
return
}
const errorMessage = !isValidJson ? 'Invalid JSON' : syntaxError
const timeoutId = setTimeout(() => {
onValidationChange(false, errorMessage)
onValidationChange(false)
}, 150)
return () => clearTimeout(timeoutId)
}, [isValidJson, syntaxError, onValidationChange, shouldValidateJson])
}, [isValidJson, onValidationChange, shouldValidateJson])
useEffect(() => {
handleStreamStartRef.current = () => {

View File

@@ -189,7 +189,7 @@ const getPreviewValue = (
* Renders the label with optional validation and description tooltips.
*
* @param config - The sub-block configuration defining the label content
* @param codeValidation - Validation state for code blocks (valid flag + optional error message)
* @param isValidJson - Whether the JSON content is valid (for code blocks)
* @param subBlockValues - Current values of all subblocks for evaluating conditional requirements
* @param wandState - State and handlers for the inline AI generate feature
* @param canonicalToggle - Metadata and handlers for the basic/advanced mode toggle
@@ -200,7 +200,7 @@ const getPreviewValue = (
*/
const renderLabel = (
config: SubBlockConfig,
codeValidation: { isValid: boolean; errorMessage: string | null },
isValidJson: boolean,
subBlockValues?: Record<string, any>,
wandState?: {
isSearchActive: boolean
@@ -250,18 +250,21 @@ const renderLabel = (
{config.title}
{required && <span className='ml-0.5'>*</span>}
{labelSuffix}
{config.type === 'code' && !codeValidation.isValid && !wandState?.isStreaming && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='inline-flex'>
<AlertTriangle className='h-3 w-3 flex-shrink-0 cursor-pointer text-destructive' />
</span>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>{codeValidation.errorMessage ?? 'Syntax error'}</p>
</Tooltip.Content>
</Tooltip.Root>
)}
{config.type === 'code' &&
config.language === 'json' &&
!isValidJson &&
!wandState?.isStreaming && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='inline-flex'>
<AlertTriangle className='h-3 w-3 flex-shrink-0 cursor-pointer text-destructive' />
</span>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>Invalid JSON</p>
</Tooltip.Content>
</Tooltip.Root>
)}
</Label>
<div className='flex min-w-0 flex-1 items-center justify-end gap-[6px]'>
{showCopy && (
@@ -463,8 +466,7 @@ function SubBlockComponent({
const params = useParams()
const workspaceId = params.workspaceId as string
const [isValidCode, setIsValidCode] = useState(true)
const [codeErrorMessage, setCodeErrorMessage] = useState<string | null>(null)
const [isValidJson, setIsValidJson] = useState(true)
const [isSearchActive, setIsSearchActive] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [copied, setCopied] = useState(false)
@@ -482,9 +484,8 @@ function SubBlockComponent({
e.stopPropagation()
}
const handleValidationChange = (isValid: boolean, errorMessage?: string | null): void => {
setIsValidCode(isValid)
setCodeErrorMessage(errorMessage ?? null)
const handleValidationChange = (isValid: boolean): void => {
setIsValidJson(isValid)
}
const isWandEnabled = config.wandConfig?.enabled ?? false
@@ -1150,7 +1151,7 @@ function SubBlockComponent({
<div onMouseDown={handleMouseDown} className='subblock-content flex flex-col gap-[10px]'>
{renderLabel(
config,
{ isValid: isValidCode, errorMessage: codeErrorMessage },
isValidJson,
subBlockValues,
{
isSearchActive,

View File

@@ -1,197 +0,0 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { sanitizeForParsing, validateJavaScript, validatePython } from './utils'
describe('sanitizeForParsing', () => {
it('replaces <Block.output> references with valid identifiers', () => {
const result = sanitizeForParsing('const x = <Block.output>')
expect(result).not.toContain('<')
expect(result).not.toContain('>')
expect(result).toContain('__placeholder_')
})
it('replaces {{ENV_VAR}} with valid identifiers', () => {
const result = sanitizeForParsing('const url = {{API_URL}}')
expect(result).not.toContain('{{')
expect(result).not.toContain('}}')
expect(result).toContain('__placeholder_')
})
it('replaces nested path references like <Block.output[0].field>', () => {
const result = sanitizeForParsing('const x = <Agent.response.choices[0].text>')
expect(result).not.toContain('<Agent')
})
it('replaces loop/parallel context references', () => {
const result = sanitizeForParsing('const item = <loop.currentItem>')
expect(result).not.toContain('<loop')
})
it('replaces variable references', () => {
const result = sanitizeForParsing('const v = <variable.myVar>')
expect(result).not.toContain('<variable')
})
it('handles multiple references in one string', () => {
const code = 'const a = <Block1.out>; const b = {{SECRET}}; const c = <Block2.value>'
const result = sanitizeForParsing(code)
expect(result).not.toContain('<Block1')
expect(result).not.toContain('{{SECRET}}')
expect(result).not.toContain('<Block2')
expect(result.match(/__placeholder_/g)?.length).toBe(3)
})
it('does not replace regular JS comparison operators', () => {
const code = 'if (a < b && c > d) {}'
const result = sanitizeForParsing(code)
expect(result).toBe(code)
})
it('does not replace HTML tags that are not references', () => {
const code = 'const html = "<div>hello</div>"'
const result = sanitizeForParsing(code)
expect(result).toBe(code)
})
})
describe('validateJavaScript', () => {
it('returns null for valid JavaScript', () => {
expect(validateJavaScript('const x = 1')).toBeNull()
expect(validateJavaScript('function foo() { return 42 }')).toBeNull()
expect(validateJavaScript('const arr = [1, 2, 3].map(x => x * 2)')).toBeNull()
})
it('returns null for valid async/await code', () => {
expect(validateJavaScript('async function foo() { await bar() }')).toBeNull()
})
it('returns null for bare return statements (function block wraps in async fn)', () => {
expect(validateJavaScript('return 42')).toBeNull()
expect(validateJavaScript(sanitizeForParsing('return <Block.output>'))).toBeNull()
expect(validateJavaScript('const x = 1\nreturn x')).toBeNull()
})
it('returns null for await at top level (wrapped in async fn)', () => {
expect(validateJavaScript('const res = await fetch("url")')).toBeNull()
})
it('returns null for valid ES module syntax', () => {
expect(validateJavaScript('import { foo } from "bar"')).toBeNull()
expect(validateJavaScript('export default function() {}')).toBeNull()
})
it('detects missing closing brace', () => {
const result = validateJavaScript('function foo() {')
expect(result).not.toBeNull()
expect(result).toContain('Syntax error')
})
it('detects missing closing paren', () => {
const result = validateJavaScript('console.log("hello"')
expect(result).not.toBeNull()
expect(result).toContain('Syntax error')
})
it('detects unexpected token', () => {
const result = validateJavaScript('const = 5')
expect(result).not.toBeNull()
expect(result).toContain('Syntax error')
})
it('includes adjusted line and column in error message', () => {
const result = validateJavaScript('const x = 1\nconst = 5')
expect(result).toMatch(/line 2/)
expect(result).toMatch(/col \d+/)
})
it('returns null for empty code', () => {
expect(validateJavaScript('')).toBeNull()
})
it('does not error on sanitized references', () => {
const code = sanitizeForParsing('const x = <Block.output> + {{ENV_VAR}}')
expect(validateJavaScript(code)).toBeNull()
})
})
describe('validatePython', () => {
it('returns null for valid Python', () => {
expect(validatePython('x = 1')).toBeNull()
expect(validatePython('def foo():\n return 42')).toBeNull()
expect(validatePython('arr = [1, 2, 3]')).toBeNull()
})
it('returns null for Python with comments', () => {
expect(validatePython('x = 1 # this is a comment')).toBeNull()
expect(validatePython('# full line comment\nx = 1')).toBeNull()
})
it('returns null for Python with strings containing brackets', () => {
expect(validatePython('x = "hello (world)"')).toBeNull()
expect(validatePython("x = 'brackets [here] {too}'")).toBeNull()
})
it('returns null for triple-quoted strings', () => {
expect(validatePython('x = """hello\nworld"""')).toBeNull()
expect(validatePython("x = '''multi\nline\nstring'''")).toBeNull()
})
it('returns null for triple-quoted strings with brackets', () => {
expect(validatePython('x = """has { and ( inside"""')).toBeNull()
})
it('detects unmatched opening paren', () => {
const result = validatePython('foo(1, 2')
expect(result).not.toBeNull()
expect(result).toContain("'('")
})
it('detects unmatched closing paren', () => {
const result = validatePython('foo)')
expect(result).not.toBeNull()
expect(result).toContain("')'")
})
it('detects unmatched bracket', () => {
const result = validatePython('arr = [1, 2')
expect(result).not.toBeNull()
expect(result).toContain("'['")
})
it('detects unterminated string', () => {
const result = validatePython('x = "hello')
expect(result).not.toBeNull()
expect(result).toContain('Unterminated string')
})
it('detects unterminated triple-quoted string', () => {
const result = validatePython('x = """hello')
expect(result).not.toBeNull()
expect(result).toContain('Unterminated triple-quoted string')
})
it('includes line number in error', () => {
const result = validatePython('x = 1\ny = (2')
expect(result).toMatch(/line 2/)
})
it('handles escaped quotes in strings', () => {
expect(validatePython('x = "hello \\"world\\""')).toBeNull()
expect(validatePython("x = 'it\\'s fine'")).toBeNull()
})
it('handles brackets inside comments', () => {
expect(validatePython('x = 1 # unmatched ( here')).toBeNull()
})
it('returns null for empty code', () => {
expect(validatePython('')).toBeNull()
})
it('does not error on sanitized references', () => {
const code = sanitizeForParsing('x = <Block.output> + {{ENV_VAR}}')
expect(validatePython(code)).toBeNull()
})
})

View File

@@ -1,17 +1,3 @@
import { parse } from 'acorn'
/**
* Matches Sim block references: `<word.path>`, `<word.path[0].nested>`, `<loop.index>`, etc.
* Must contain a dot (.) to distinguish from HTML tags or comparison operators.
*/
const REFERENCE_PATTERN = /<[a-zA-Z]\w*(?:\.\w+(?:\[\d+\])?)+>/g
/**
* Matches Sim env-var placeholders: `{{WORD}}`, `{{MY_VAR}}`.
* Only allows word characters (no spaces, special chars).
*/
const ENV_VAR_PATTERN = /\{\{\w+\}\}/g
/**
* Restores the cursor position in a textarea after a dropdown insertion.
* Schedules a macrotask (via setTimeout) that runs after React's controlled-component commit
@@ -32,132 +18,3 @@ export function restoreCursorAfterInsertion(
}
}, 0)
}
/**
* Replaces `<Block.output>` references and `{{ENV_VAR}}` placeholders with
* valid JS/Python identifiers so the code can be parsed without false errors.
*/
export function sanitizeForParsing(code: string): string {
let counter = 0
let sanitized = code.replace(ENV_VAR_PATTERN, () => `__placeholder_${counter++}__`)
sanitized = sanitized.replace(REFERENCE_PATTERN, () => `__placeholder_${counter++}__`)
return sanitized
}
/**
* Validates JavaScript code for syntax errors using acorn.
*
* Tries two parse strategies to match the Function block's runtime behavior:
* 1. As a module (`import`/`export` are valid at top level)
* 2. Wrapped in `async () => { ... }` (bare `return`/`await` are valid)
*
* Only reports an error if both strategies fail, using the wrapped error
* since that's the primary execution context.
*
* @returns Error message string, or null if valid.
*/
export function validateJavaScript(code: string): string | null {
try {
parse(code, { ecmaVersion: 'latest', sourceType: 'module' })
return null
} catch {
// Module parse failed — try as function body (allows bare return/await)
}
const wrapped = `(async () => {\n${code}\n})()`
try {
parse(wrapped, { ecmaVersion: 'latest', sourceType: 'script' })
return null
} catch (e: unknown) {
if (e instanceof SyntaxError) {
const msg = e.message
const match = msg.match(/\((\d+):(\d+)\)/)
if (match) {
const adjustedLine = Number(match[1]) - 1
if (adjustedLine < 1) return null
return `Syntax error at line ${adjustedLine}, col ${match[2]}: ${msg.replace(/\s*\(\d+:\d+\)/, '')}`
}
return `Syntax error: ${msg}`
}
return null
}
}
/**
* Validates Python code for common syntax errors: unmatched brackets/parens,
* unterminated strings (single-line and triple-quoted).
* Processes the entire code string as a stream to correctly handle
* multiline triple-quoted strings.
*
* @returns Error message string, or null if no issues detected.
*/
export function validatePython(code: string): string | null {
const stack: { char: string; line: number }[] = []
const openers: Record<string, string> = { ')': '(', ']': '[', '}': '{' }
const closers = new Set([')', ']', '}'])
const openChars = new Set(['(', '[', '{'])
let line = 1
let i = 0
while (i < code.length) {
const ch = code[i]
if (ch === '\n') {
line++
i++
continue
}
if (ch === '#') {
const newline = code.indexOf('\n', i)
i = newline === -1 ? code.length : newline
continue
}
if (ch === '"' || ch === "'") {
const tripleQuote = ch.repeat(3)
if (code.slice(i, i + 3) === tripleQuote) {
const startLine = line
const endIdx = code.indexOf(tripleQuote, i + 3)
if (endIdx === -1) {
return `Unterminated triple-quoted string starting at line ${startLine}`
}
for (let k = i; k < endIdx + 3; k++) {
if (code[k] === '\n') line++
}
i = endIdx + 3
continue
}
const startLine = line
i++
while (i < code.length && code[i] !== ch && code[i] !== '\n') {
if (code[i] === '\\') i++
i++
}
if (i >= code.length || code[i] === '\n') {
return `Unterminated string at line ${startLine}`
}
i++
continue
}
if (openChars.has(ch)) {
stack.push({ char: ch, line })
} else if (closers.has(ch)) {
if (stack.length === 0 || stack[stack.length - 1].char !== openers[ch]) {
return `Unmatched '${ch}' at line ${line}`
}
stack.pop()
}
i++
}
if (stack.length > 0) {
const unmatched = stack[stack.length - 1]
return `Unmatched '${unmatched.char}' opened at line ${unmatched.line}`
}
return null
}

View File

@@ -151,7 +151,7 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
: undefined
return (
<div className='group relative'>
<div className='group pointer-events-none relative'>
<div
ref={blockRef}
className={cn(

View File

@@ -82,7 +82,6 @@
"@trigger.dev/sdk": "4.1.2",
"@types/react-window": "2.0.0",
"@types/three": "0.177.0",
"acorn": "8.16.0",
"better-auth": "1.3.12",
"binary-extensions": "^2.0.0",
"browser-image-compression": "^2.0.2",

View File

@@ -115,7 +115,6 @@
"@trigger.dev/sdk": "4.1.2",
"@types/react-window": "2.0.0",
"@types/three": "0.177.0",
"acorn": "8.16.0",
"better-auth": "1.3.12",
"binary-extensions": "^2.0.0",
"browser-image-compression": "^2.0.2",
@@ -1642,7 +1641,7 @@
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
@@ -3826,8 +3825,6 @@
"@langchain/core/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
"@mdx-js/mdx/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
@@ -4082,8 +4079,6 @@
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"esast-util-from-js/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -4144,8 +4139,6 @@
"imapflow/pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="],
"import-in-the-middle/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"inquirer/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
@@ -4180,12 +4173,8 @@
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"micromark-extension-mdxjs/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"neo4j-driver-bolt-connection/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],