@@ -53,12 +53,25 @@ interface StoreEntry {
5353
5454
5555function platformFnToDefinition ( fn : FunctionNode ) : NodeDefinition {
56+ const inputs : Array < { name : string ; type : string } > = [ ] ;
57+ const secrets = ( fn . requiredSecrets ?? [ ] ) as FunctionRequirement [ ] ;
58+ const configs = ( fn . requiredConfigs ?? [ ] ) as FunctionRequirement [ ] ;
59+
60+ for ( const s of secrets ) {
61+ if ( s . name ) inputs . push ( { name : s . name , type : 'string' } ) ;
62+ }
63+ for ( const c of configs ) {
64+ if ( c . name ) inputs . push ( { name : c . name , type : 'string' } ) ;
65+ }
66+ // Fallback: if no fields declared, expose a generic payload input
67+ if ( inputs . length === 0 ) inputs . push ( { name : 'payload' , type : 'json' } ) ;
68+
5669 return {
5770 context : fn . scope || 'platform' ,
5871 name : fn . taskIdentifier || fn . name || '' ,
5972 category : 'functions' ,
6073 description : fn . description || undefined ,
61- inputs : [ { name : 'payload' , type : 'json' } ] ,
74+ inputs,
6275 outputs : [ { name : 'result' , type : 'json' } ] ,
6376 icon : fn . isInvocable ? 'zap' : 'circle' ,
6477 } ;
@@ -130,6 +143,8 @@ const FUNCTION_FIELDS = {
130143 isBuiltIn : true ,
131144 scope : true ,
132145 description : true ,
146+ requiredSecrets : true ,
147+ requiredConfigs : true ,
133148} as const ;
134149
135150const STORE_FIELDS = { id : true , name : true , hash : true } as const ;
@@ -336,7 +351,9 @@ async function deleteStore(id: string): Promise<void> {
336351
337352async function importAndExecuteGraph (
338353 graph : Graph ,
339- inputPayload : Record < string , unknown > = { }
354+ inputPayload : Record < string , unknown > = { } ,
355+ outputNode : string | null = null ,
356+ outputPort : string | null = null ,
340357) : Promise < { graphId : string ; executionId : string } > {
341358 const endpoint = '/graphql/compute' ;
342359
@@ -354,18 +371,15 @@ async function importAndExecuteGraph(
354371 } ) ;
355372 const graphId = importData . platformImportGraphJson . result ;
356373
374+ const execInput : Record < string , unknown > = { graphId, inputPayload } ;
375+ if ( outputNode ) execInput . outputNode = outputNode ;
376+ if ( outputPort ) execInput . outputPort = outputPort ;
377+
357378 const execData = await gqlFetch ( endpoint , `
358379 mutation ($input: PlatformStartExecutionInput!) {
359380 platformStartExecution(input: $input) { result }
360381 }
361- ` , {
362- input : {
363- graphId,
364- inputPayload,
365- outputNode : 'output_result' ,
366- outputPort : 'value' ,
367- } ,
368- } ) ;
382+ ` , { input : execInput } ) ;
369383 const executionId = execData . platformStartExecution . result ;
370384
371385 return { graphId, executionId } ;
@@ -443,6 +457,8 @@ export function FlowsPanel() {
443457 const [ isLoadingFlow , setIsLoadingFlow ] = useState ( false ) ;
444458 const [ collapsedCategories , setCollapsedCategories ] = useState < Set < string > > ( new Set ( ) ) ;
445459 const [ saveStatus , setSaveStatus ] = useState < string | null > ( null ) ;
460+ const [ autoSave , setAutoSave ] = useState ( false ) ;
461+ const autoSaveTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
446462 const [ executionState , setExecutionState ] = useState < ExecutionState | null > ( null ) ;
447463 const [ isExecuting , setIsExecuting ] = useState ( false ) ;
448464 const pollRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
@@ -541,6 +557,7 @@ export function FlowsPanel() {
541557 setCurrentGraph ( graph ) ;
542558 } , [ ] ) ;
543559
560+
544561 const handleEvaluate = useCallback ( async ( ) => {
545562 const graph = graphRef . current ;
546563 if ( ! graph . nodes . length ) return ;
@@ -589,7 +606,14 @@ export function FlowsPanel() {
589606 }
590607 }
591608
592- const { executionId } = await importAndExecuteGraph ( graph , inputPayload ) ;
609+ // Auto-detect output node — use first graphOutput if present, otherwise omit
610+ const outputNode = graph . nodes . find ( n => n . type === 'graphOutput' ) ;
611+ const { executionId } = await importAndExecuteGraph (
612+ graph ,
613+ inputPayload ,
614+ outputNode ?. name ?? null ,
615+ outputNode ? 'value' : null ,
616+ ) ;
593617
594618 // Initialize node states — mark all non-boundary nodes as pending
595619 const initialNodeStates : Record < string , NodeState > = { } ;
@@ -671,6 +695,18 @@ export function FlowsPanel() {
671695 }
672696 } , [ flowName , stores . length , activeStoreId , activeCommitId , refetchStores ] ) ;
673697
698+ // Auto-save: debounce 1s after each graph edit
699+ useEffect ( ( ) => {
700+ if ( ! autoSave || ! activeStoreId ) return ;
701+ if ( autoSaveTimerRef . current ) clearTimeout ( autoSaveTimerRef . current ) ;
702+ autoSaveTimerRef . current = setTimeout ( ( ) => {
703+ handleSave ( ) ;
704+ } , 1000 ) ;
705+ return ( ) => {
706+ if ( autoSaveTimerRef . current ) clearTimeout ( autoSaveTimerRef . current ) ;
707+ } ;
708+ } , [ currentGraph , autoSave , activeStoreId , handleSave ] ) ;
709+
674710 const handleNew = useCallback ( ( ) => {
675711 setCurrentGraph ( { ...DEFAULT_GRAPH , name : '' } ) ;
676712 setFlowName ( '' ) ;
@@ -743,6 +779,15 @@ export function FlowsPanel() {
743779 { isSaving ? < RefreshCw size = { 14 } className = "animate-spin" /> : < Save size = { 14 } /> }
744780 { isSaving ? 'Saving...' : 'Save' }
745781 </ button >
782+ < label className = "flex items-center gap-1.5 text-xs text-zinc-400 cursor-pointer select-none" >
783+ < input
784+ type = "checkbox"
785+ checked = { autoSave }
786+ onChange = { ( e ) => setAutoSave ( e . target . checked ) }
787+ className = "accent-green-500 w-3.5 h-3.5"
788+ />
789+ Auto
790+ </ label >
746791 < button
747792 onClick = { handleEvaluate }
748793 disabled = { isEvaluating }
0 commit comments