feat: add dual-mode tree editor with Code Mode, variables, and markdown sync

Implements the full dual-mode tree editor (Plan Phases 1-5):

Backend:
- JSONB↔Markdown bidirectional serializer/parser with mistune
- Markdown validator with line/column error reporting
- 3 API endpoints: export-markdown, import-markdown, validate-markdown
- Variable extraction/resolution service ([USER_INPUT], [VAR], [SAVE_AS])
- Session variables JSONB column (migration 028)
- 39 tree markdown tests + variable service tests (403 total passing)

Frontend:
- Monaco-based Code Mode with custom Monarch tokenizer and dark theme
- Autocomplete for @node_id refs, type values, variable names
- Debounced validation (800ms) with inline Monaco error markers
- Syntax help panel (absolute overlay, toggleable)
- Starter template for new trees with valid cross-references
- Bidirectional metadata sync (name/description/category/tags frontmatter)
- Synchronous tree→markdown serializer (fixes async race condition)
- Pre-save validation blocks save on broken refs or missing tree name
- Mode-aware undo/redo: Monaco native in Code Mode, throttled zundo in Flow Mode
- Variable prompt modal and frontend resolver for session navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-10 09:45:26 -05:00
parent 2bd47004e7
commit eac6e184ec
32 changed files with 3369 additions and 52 deletions

View File

@@ -0,0 +1,73 @@
import { useState } from 'react'
import { cn } from '@/lib/utils'
interface VariablePromptModalProps {
/** The prompt text from [USER_INPUT:prompt] */
prompt: string
/** Called with the user's input value */
onSubmit: (value: string) => void
/** Called when user cancels */
onCancel: () => void
}
export function VariablePromptModal({ prompt, onSubmit, onCancel }: VariablePromptModalProps) {
const [value, setValue] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (value.trim()) {
onSubmit(value.trim())
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="glass-card w-full max-w-md rounded-2xl p-6 shadow-lg">
<h2 className="mb-1 text-lg font-semibold text-white">Input Required</h2>
<p className="mb-4 text-sm text-white/40">
This step requires you to provide a value.
</p>
<form onSubmit={handleSubmit}>
<label className="mb-2 block text-sm font-medium text-white/70">
{prompt}
</label>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Enter value..."
autoFocus
className={cn(
'w-full rounded-lg border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/30 focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
/>
<div className="mt-4 flex gap-2">
<button
type="submit"
disabled={!value.trim()}
className={cn(
'flex-1 rounded-lg bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
Continue
</button>
<button
type="button"
onClick={onCancel}
className={cn(
'rounded-lg border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white'
)}
>
Skip
</button>
</div>
</form>
</div>
</div>
)
}