Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { Combobox, FieldDivider, Label, Slider, Switch } from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { useParams } from 'next/navigation'
Expand Down Expand Up @@ -42,6 +42,27 @@ function requiresJsonValue(paramSchema: any): boolean {
)
}

/**
* Stable signature of an entire tool schema, for detecting whether the effective
* param shape has changed (independent of object identity). Signs the whole schema
* rather than cherry-picking fields (e.g. just `properties`) so a refresh that only
* changes `required`, or any other schema-level field, isn't silently missed.
*/
function schemaSignature(schema: unknown): string {
return schema ? JSON.stringify(schema) : ''
}

/**
* True when text looks like an attempted JSON array/object literal (starts with `[`
* or `{`), as opposed to plain freeform text. Used to tell an in-progress, incomplete
* JSON literal (which must not persist until valid — see `requiresJsonValue`) apart
* from the comma-separated/plain-text shorthand array params also accept.
*/
function looksLikeJsonLiteral(text: string): boolean {
const trimmed = text.trim()
return trimmed.startsWith('[') || trimmed.startsWith('{')
}
Comment thread
waleedlatif1 marked this conversation as resolved.

interface McpDynamicArgsProps {
blockId: string
subBlockId: string
Expand Down Expand Up @@ -97,6 +118,58 @@ export function McpDynamicArgs({
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
const toolSchema = cachedSchema || selectedToolConfig?.inputSchema

/**
* Draft text for JSON-value params (object/array/non-primitive-enum) whose current
* edit isn't valid JSON yet, paired with a signature of the persisted value it was
* typed against. Keeping this out of toolArgs means the stored argument is always
* either the last valid parsed value or untouched — never malformed text that could
* reach tool execution. A draft is only displayed while its baseline still matches
* the live persisted value, so an external change to that value (undo/redo, a diff
* baseline switch, a collaborator's edit) can't be shadowed by stale draft text.
* Drafts also reset wholesale on either of two independent triggers:
* - the selected tool or the cached `_toolSchema` snapshot changes (this pair
* always drives `toolSchema` whenever a cached snapshot exists) — the live
* schema tracker is also re-baselined to the new tool's current signature
* here (even if still empty), so a tool switch never leaves the *previous*
* tool's signature behind to be misread as a "refresh" once the new tool's
* schema loads a moment later, or
* - for the *same* tool, the live discovered schema's signature changes to a
* different non-empty value than the last non-empty value actually observed
* — a genuine re-discovery/refresh. Comparing against the last known
* non-empty value (rather than merely the previous render's value) means a
* schema that transiently disappears and reappears — e.g. `mcpTools`
* refetching — still resets drafts if it comes back different, while a
* plain empty → non-empty transition (the initial async load) does not,
* since there is no prior non-empty value for this tool to compare against.
*/
const [invalidJsonDrafts, setInvalidJsonDrafts] = useState<
Record<string, { text: string; baseline: string }>
>({})
const toolAndCachedSchemaKey = `${selectedTool ?? ''}|${schemaSignature(cachedSchema)}`
const liveSchemaSignature = schemaSignature(selectedToolConfig?.inputSchema)
const [prevToolAndCachedSchemaKey, setPrevToolAndCachedSchemaKey] =
useState(toolAndCachedSchemaKey)
const [lastNonEmptyLiveSchemaSignature, setLastNonEmptyLiveSchemaSignature] =
useState(liveSchemaSignature)
if (prevToolAndCachedSchemaKey !== toolAndCachedSchemaKey) {
setInvalidJsonDrafts({})
setPrevToolAndCachedSchemaKey(toolAndCachedSchemaKey)
setLastNonEmptyLiveSchemaSignature(liveSchemaSignature)
} else {
const nextLastNonEmptyLiveSchemaSignature =
liveSchemaSignature !== '' ? liveSchemaSignature : lastNonEmptyLiveSchemaSignature
if (nextLastNonEmptyLiveSchemaSignature !== lastNonEmptyLiveSchemaSignature) {
const isGenuineLiveRefresh =
liveSchemaSignature !== '' &&
lastNonEmptyLiveSchemaSignature !== '' &&
liveSchemaSignature !== lastNonEmptyLiveSchemaSignature
if (isGenuineLiveRefresh) {
setInvalidJsonDrafts({})
}
setLastNonEmptyLiveSchemaSignature(nextLastNonEmptyLiveSchemaSignature)
}
}

const currentArgs = useCallback(() => {
if (isPreview && previewValue) {
if (typeof previewValue === 'string') {
Expand Down Expand Up @@ -158,7 +231,7 @@ export function McpDynamicArgs({
if (paramSchema.maxLength && paramSchema.maxLength > 100) return 'long-input'
return 'short-input'
}
if (paramSchema.type === 'array') return 'long-input'
if (paramSchema.type === 'array' || paramSchema.type === 'object') return 'long-input'
return 'short-input'
}

Expand Down Expand Up @@ -270,8 +343,17 @@ export function McpDynamicArgs({

case 'long-input': {
const config = createParamConfig(paramName, paramSchema, 'long-input')
const needsJsonValue = requiresJsonValue(paramSchema)
const valueSignature = JSON.stringify(value ?? null)
const draft = invalidJsonDrafts[paramName]
const activeDraft =
needsJsonValue && draft && draft.baseline === valueSignature ? draft.text : undefined
const displayValue =
typeof value === 'string' || value == null ? value || '' : JSON.stringify(value)
activeDraft !== undefined
? activeDraft
: typeof value === 'string' || value == null
? value || ''
: JSON.stringify(value)
Comment thread
waleedlatif1 marked this conversation as resolved.
return (
<LongInput
key={`${paramName}-long`}
Expand All @@ -282,14 +364,34 @@ export function McpDynamicArgs({
rows={4}
value={displayValue}
onChange={(newValue) => {
if (!requiresJsonValue(paramSchema)) {
if (!needsJsonValue) {
updateParameter(paramName, newValue)
return
}
const clearDraft = () =>
setInvalidJsonDrafts((prev) => {
if (!(paramName in prev)) return prev
const { [paramName]: _removed, ...rest } = prev
return rest
})
if (newValue === '') {
updateParameter(paramName, '')
clearDraft()
return
}
try {
updateParameter(paramName, JSON.parse(newValue))
clearDraft()
} catch {
updateParameter(paramName, newValue)
if (paramSchema.type === 'array' && !looksLikeJsonLiteral(newValue)) {
updateParameter(paramName, newValue)
clearDraft()
return
}
setInvalidJsonDrafts((prev) => ({
...prev,
[paramName]: { text: newValue, baseline: valueSignature },
}))
Comment thread
waleedlatif1 marked this conversation as resolved.
}
}}
isPreview={isPreview}
Expand Down
Loading