11// Bespoke Agent Builder - Client Logic
22
3- import type { WorkflowConnection , WorkflowNode , WorkflowRunResult } from '@agentic/types' ;
3+ import type { WorkflowConnection , WorkflowGraph , WorkflowNode , WorkflowRunResult } from '@agentic/types' ;
44import { runWorkflowStream , resumeWorkflow , fetchConfig , fetchRun } from '../services/api' ;
55import { renderMarkdown , escapeHtml } from './markdown' ;
66
@@ -223,6 +223,8 @@ export class WorkflowEditor {
223223
224224 private currentRunId : string | null ;
225225
226+ private activeRunGraph : WorkflowGraphPayload | null ;
227+
226228 private runHistory : RunHistoryEntry [ ] ;
227229
228230 private getErrorMessage ( error : unknown ) : string {
@@ -241,6 +243,36 @@ export class WorkflowEditor {
241243 } ) ;
242244 }
243245
246+ private cloneGraphPayload ( graph : WorkflowGraphPayload ) : WorkflowGraphPayload {
247+ return JSON . parse ( JSON . stringify ( graph ) ) as WorkflowGraphPayload ;
248+ }
249+
250+ private setActiveRunGraph ( graph : WorkflowGraphPayload | null ) : void {
251+ this . activeRunGraph = graph ? this . cloneGraphPayload ( graph ) : null ;
252+ }
253+
254+ private syncActiveRunGraphFromResult ( result : WorkflowRunResult ) : void {
255+ const workflow : WorkflowGraph | undefined = result . workflow ;
256+ if ( ! workflow || ! Array . isArray ( workflow . nodes ) || ! Array . isArray ( workflow . connections ) ) return ;
257+ this . setActiveRunGraph ( {
258+ nodes : workflow . nodes as EditorNode [ ] ,
259+ connections : workflow . connections
260+ } ) ;
261+ }
262+
263+ private getRunNodes ( ) : EditorNode [ ] {
264+ return this . activeRunGraph ?. nodes ?? this . nodes ;
265+ }
266+
267+ private getRunConnections ( ) : WorkflowConnection [ ] {
268+ return this . activeRunGraph ?. connections ?? this . connections ;
269+ }
270+
271+ private getRunNodeById ( nodeId : string | null | undefined ) : EditorNode | undefined {
272+ if ( ! nodeId ) return undefined ;
273+ return this . getRunNodes ( ) . find ( ( node ) => node . id === nodeId ) ;
274+ }
275+
244276 constructor ( ) {
245277 this . modelOptions = [ ...DEFAULT_MODEL_OPTIONS ] ;
246278 this . modelEfforts = { ...DEFAULT_MODEL_EFFORTS } ;
@@ -279,6 +311,7 @@ export class WorkflowEditor {
279311 this . activeRunController = null ;
280312 this . lastLlmResponseContent = null ;
281313 this . currentRunId = null ;
314+ this . activeRunGraph = null ;
282315 this . runHistory = [ ] ;
283316
284317 this . splitPanelCtorPromise = null ;
@@ -431,7 +464,7 @@ export class WorkflowEditor {
431464 }
432465
433466 getPrimaryAgentName ( ) {
434- const agentNode = this . nodes . find ( ( n ) => n . type === 'agent' ) ;
467+ const agentNode = this . getRunNodes ( ) . find ( ( n ) => n . type === 'agent' ) ;
435468 if ( agentNode && agentNode . data ) {
436469 const name = ( agentNode . data . agentName || '' ) . trim ( ) ;
437470 if ( name ) return name ;
@@ -651,6 +684,7 @@ export class WorkflowEditor {
651684 this . clearApprovalMessage ( ) ;
652685 this . appendStatusMessage ( 'Cancelled' ) ;
653686 this . currentRunId = null ;
687+ this . setActiveRunGraph ( null ) ;
654688 this . setWorkflowState ( 'idle' ) ;
655689 }
656690 }
@@ -724,6 +758,16 @@ export class WorkflowEditor {
724758 return null ;
725759 }
726760
761+ getClearDisableReason ( ) : string | null {
762+ if ( this . workflowState === 'running' ) {
763+ return 'Cannot clear canvas while workflow is running.' ;
764+ }
765+ if ( this . workflowState === 'paused' ) {
766+ return 'Cannot clear canvas while workflow is paused waiting for approval.' ;
767+ }
768+ return null ;
769+ }
770+
727771 updateRunButton ( ) {
728772 if ( ! this . runButton ) return ;
729773 if ( this . cancelRunButton ) {
@@ -733,9 +777,7 @@ export class WorkflowEditor {
733777 }
734778
735779 if ( this . clearButton ) {
736- const clearDisabledReason = this . workflowState === 'running'
737- ? 'Cannot clear canvas while workflow is running.'
738- : null ;
780+ const clearDisabledReason = this . getClearDisableReason ( ) ;
739781 this . clearButton . disabled = Boolean ( clearDisabledReason ) ;
740782 this . setClearButtonHint ( clearDisabledReason ) ;
741783 }
@@ -935,7 +977,7 @@ export class WorkflowEditor {
935977 }
936978 if ( this . clearButton ) {
937979 this . clearButton . addEventListener ( 'click' , async ( ) => {
938- if ( this . workflowState === 'running ') return ;
980+ if ( this . workflowState !== 'idle ') return ;
939981 const confirmed = await this . openConfirmModal ( {
940982 title : 'Clear Canvas' ,
941983 message : 'Remove all nodes and connections from the canvas?' ,
@@ -1326,7 +1368,8 @@ export class WorkflowEditor {
13261368 duplicateBtn . innerHTML = '<span class="icon icon-content icon-primary"></span>' ;
13271369 duplicateBtn . title = 'Duplicate Agent' ;
13281370 duplicateBtn . setAttribute ( 'data-tooltip' , 'Duplicate Agent' ) ;
1329- duplicateBtn . addEventListener ( 'mousedown' , ( e : any ) => {
1371+ duplicateBtn . setAttribute ( 'aria-label' , 'Duplicate Agent' ) ;
1372+ duplicateBtn . addEventListener ( 'click' , ( e : any ) => {
13301373 e . stopPropagation ( ) ;
13311374 this . duplicateAgentNode ( node ) ;
13321375 } ) ;
@@ -2031,7 +2074,7 @@ export class WorkflowEditor {
20312074 showApprovalMessage ( nodeId : any ) {
20322075 if ( ! this . chatMessages ) return ;
20332076 this . clearApprovalMessage ( ) ;
2034- const node = this . nodes . find ( ( n : any ) => n . id === nodeId ) ;
2077+ const node = this . getRunNodeById ( nodeId ) ;
20352078 const messageText = node ?. data ?. prompt || 'Approval required before continuing.' ;
20362079
20372080 const message = document . createElement ( 'div' ) ;
@@ -2093,11 +2136,11 @@ export class WorkflowEditor {
20932136 }
20942137
20952138 getApprovalNextNode ( nodeId : string , decision : 'approve' | 'reject' ) : EditorNode | undefined {
2096- const connection = this . connections . find (
2139+ const connection = this . getRunConnections ( ) . find (
20972140 ( conn : any ) => conn . source === nodeId && conn . sourceHandle === decision
20982141 ) ;
20992142 if ( ! connection ) return undefined ;
2100- return this . nodes . find ( ( node : any ) => node . id === connection . target ) ;
2143+ return this . getRunNodes ( ) . find ( ( node : any ) => node . id === connection . target ) ;
21012144 }
21022145
21032146 extractWaitingNodeId ( logs : any = [ ] ) {
@@ -2174,12 +2217,15 @@ export class WorkflowEditor {
21742217
21752218 isApprovalInputLog ( entry : any ) : boolean {
21762219 if ( ! entry || entry . type !== 'input_received' ) return false ;
2177- const node = this . nodes . find ( ( n : any ) => n . id === entry . nodeId ) ;
2220+ const node = this . getRunNodeById ( entry . nodeId ) ;
21782221 return node ?. type === 'approval' || node ?. type === 'input' ;
21792222 }
21802223
21812224 parseApprovalInputLog ( content : string ) : { decision : 'approve' | 'reject' ; note : string } {
2182- const decision = content . toLowerCase ( ) . includes ( 'rejected' ) ? 'reject' : 'approve' ;
2225+ const decisionPrefixMatch = content . match ( / (?: ^ | \n ) \s * (?: D e c i s i o n | S t a t u s ) \s * : \s * ( a p p r o v e | a p p r o v e d | r e j e c t | r e j e c t e d ) \b / i) ;
2226+ const sentencePrefixMatch = content . match ( / (?: ^ | \n ) \s * U s e r \s + ( a p p r o v e d | r e j e c t e d ) \b / i) ;
2227+ const rawDecision = ( decisionPrefixMatch ?. [ 1 ] || sentencePrefixMatch ?. [ 1 ] || 'approve' ) . toLowerCase ( ) ;
2228+ const decision = rawDecision . startsWith ( 'reject' ) ? 'reject' : 'approve' ;
21832229 const feedbackMatch = content . match ( / f e e d b a c k : \s * ( .* ) $ / i) ;
21842230 const note = feedbackMatch ?. [ 1 ] ?. trim ( ) || '' ;
21852231 return { decision, note } ;
@@ -2195,14 +2241,14 @@ export class WorkflowEditor {
21952241 }
21962242
21972243 getAgentNameForNode ( nodeId : string ) : string {
2198- const node = this . nodes . find ( ( n : any ) => n . id === nodeId ) ;
2244+ const node = this . getRunNodeById ( nodeId ) ;
21992245 return ( node ?. data ?. agentName || '' ) . trim ( ) || 'Agent' ;
22002246 }
22012247
22022248 onLogEntry ( entry : any ) {
22032249 const type = entry . type || '' ;
22042250 if ( type === 'step_start' ) {
2205- const node = this . nodes . find ( ( n : any ) => n . id === entry . nodeId ) ;
2251+ const node = this . getRunNodeById ( entry . nodeId ) ;
22062252 if ( node ?. type === 'agent' ) {
22072253 this . showAgentSpinner ( this . getAgentNameForNode ( entry . nodeId ) ) ;
22082254 }
@@ -2271,10 +2317,11 @@ export class WorkflowEditor {
22712317 if ( ! startNode . data ) startNode . data = { } ;
22722318 startNode . data . initialInput = this . currentPrompt ;
22732319
2274- const graph = {
2320+ const graph = this . cloneGraphPayload ( {
22752321 nodes : this . nodes ,
22762322 connections : this . connections
2277- } ;
2323+ } ) ;
2324+ this . setActiveRunGraph ( graph ) ;
22782325 const controller = new AbortController ( ) ;
22792326 this . activeRunController = controller ;
22802327
@@ -2291,6 +2338,7 @@ export class WorkflowEditor {
22912338 this . appendChatMessage ( this . getErrorMessage ( e ) , 'error' ) ;
22922339 this . appendStatusMessage ( 'Failed' , 'failed' ) ;
22932340 this . hideAgentSpinner ( ) ;
2341+ this . setActiveRunGraph ( null ) ;
22942342 this . setWorkflowState ( 'idle' ) ;
22952343 } finally {
22962344 if ( this . activeRunController === controller ) {
@@ -2300,6 +2348,7 @@ export class WorkflowEditor {
23002348 }
23012349
23022350 handleRunResult ( result : WorkflowRunResult , fromStream = false ) {
2351+ this . syncActiveRunGraphFromResult ( result ) ;
23032352 if ( ! fromStream && result . logs ) {
23042353 this . renderChatFromLogs ( result . logs ) ;
23052354 }
@@ -2324,17 +2373,20 @@ export class WorkflowEditor {
23242373 this . hideAgentSpinner ( ) ;
23252374 this . setWorkflowState ( 'idle' ) ;
23262375 this . currentRunId = null ;
2376+ this . setActiveRunGraph ( null ) ;
23272377 } else if ( result . status === 'failed' ) {
23282378 this . clearRunId ( ) ;
23292379 this . clearApprovalMessage ( ) ;
23302380 this . appendStatusMessage ( 'Failed' , 'failed' ) ;
23312381 this . hideAgentSpinner ( ) ;
23322382 this . setWorkflowState ( 'idle' ) ;
23332383 this . currentRunId = null ;
2384+ this . setActiveRunGraph ( null ) ;
23342385 } else {
23352386 this . clearApprovalMessage ( ) ;
23362387 this . hideAgentSpinner ( ) ;
23372388 this . setWorkflowState ( 'idle' ) ;
2389+ this . setActiveRunGraph ( null ) ;
23382390 }
23392391 }
23402392
@@ -2350,7 +2402,13 @@ export class WorkflowEditor {
23502402 // runId so recovery can be reattempted on the next page load.
23512403 return ;
23522404 }
2353- if ( ! result ) { this . clearRunId ( ) ; return ; } // 404 — run genuinely gone
2405+ if ( ! result ) {
2406+ this . clearRunId ( ) ;
2407+ this . setActiveRunGraph ( null ) ;
2408+ return ;
2409+ } // 404 — run genuinely gone
2410+
2411+ this . syncActiveRunGraphFromResult ( result ) ;
23542412
23552413 if ( result . status === 'running' ) {
23562414 // Engine still executing on server — show partial chat and poll for updates
@@ -2366,6 +2424,7 @@ export class WorkflowEditor {
23662424 this . renderChatFromLogs ( result . logs ) ;
23672425 this . appendStatusMessage ( 'Previous paused run was lost (server restarted).' , 'failed' ) ;
23682426 this . setWorkflowState ( 'idle' ) ;
2427+ this . setActiveRunGraph ( null ) ;
23692428 } else {
23702429 // completed, failed, or paused-with-waitingForInput —
23712430 // handleRunResult covers all three cases
@@ -2393,8 +2452,10 @@ export class WorkflowEditor {
23932452 // 404 — run is genuinely gone from server and disk
23942453 this . clearRunId ( ) ;
23952454 this . setWorkflowState ( 'idle' ) ;
2455+ this . setActiveRunGraph ( null ) ;
23962456 return ;
23972457 }
2458+ this . syncActiveRunGraphFromResult ( result ) ;
23982459 // Re-render chat if new log entries arrived since last poll
23992460 const logs = Array . isArray ( result . logs ) ? result . logs : [ ] ;
24002461 if ( logs . length > knownLogCount ) {
@@ -2434,6 +2495,7 @@ export class WorkflowEditor {
24342495 this . appendStatusMessage ( 'Failed' , 'failed' ) ;
24352496 this . hideAgentSpinner ( ) ;
24362497 this . setWorkflowState ( 'idle' ) ;
2498+ this . setActiveRunGraph ( null ) ;
24372499 } finally {
24382500 if ( this . activeRunController === controller ) {
24392501 this . activeRunController = null ;
0 commit comments