@@ -61,6 +61,7 @@ glob _lock = threading.Lock();
6161glob _ws: Any = None ;
6262glob _connected: bool = False ;
6363glob _pending: dict [str , Any] = {};
64+ glob _pending_confirms: dict [str , Any] = {};
6465glob _relay_loop: Any = None ;
6566glob _relay_ready: Any = threading.Event();
6667
@@ -196,21 +197,38 @@ def _send_command(action: str, data: dict | None = None, session_id: str = "", t
196197 }
197198 return res.get(" data" );
198199}
200+ """ Resolve a pending confirmation via chat WebSocket (called from server.jac)."""
201+ def _resolve_confirm (confirm_id : str , result : str ) -> None {
202+ entry : Any = None ;
203+ with _lock {
204+ entry = _pending_confirms.pop(confirm_id, None );
205+ }
206+ if entry is not None {
207+ (evt, res) = entry ;
208+ res[" result" ] = result;
209+ evt.set();
210+ }
211+ }
199212
200-
201- # ── Confirmation flow ─────────────────────────────────────────────────────────
202- """ Show confirm dialog and poll for result (max 30s)."""
203- def _request_confirm (provider : CanvasToolProvider, message : str ) -> bool {
213+ """ Send confirmation request via chat WebSocket and wait for response (max 60s)."""
214+ def _chat_confirm (ws : Any, event_loop : Any, session_id : str , message : str ) -> str {
204215 confirm_id: str = str (uuid.uuid4());
205- provider.call_tool(" show_confirm" , {" confirmId" : confirm_id, " message" : message});
206- deadline: float = time.time() + 30.0 ;
207- while time.time() < deadline {
208- result: dict [str , Any] = json.loads(provider.call_tool(" get_confirm_result" , {" confirmId" : confirm_id}));
209- if result.get(" result" ) == " ok" { return True ; }
210- if result.get(" result" ) == " cancel" { return False ; }
211- time.sleep(0.5 );
216+ evt: Any = threading.Event();
217+ res: dict = {};
218+ with _lock {
219+ _pending_confirms[confirm_id] = (evt, res);
220+ }
221+ _send_ws(ws, {" type" : " confirm" , " sessionId" : session_id, " confirmId" : confirm_id, " message" : message}, event_loop);
222+ if not evt.wait(timeout = 60.0 ) {
223+ with _lock {
224+ _pending_confirms.pop(confirm_id, None );
225+ }
226+ return " cancel" ;
227+ }
228+ with _lock {
229+ _pending_confirms.pop(confirm_id, None );
212230 }
213- return False ;
231+ return res.get( " result " , " cancel " ) ;
214232}
215233
216234
@@ -390,28 +408,50 @@ walker AgentOrchestrator {
390408
391409 canvas_mode: str = " replace" ;
392410 existing_elements_json: str = " []" ;
411+ existing_bounding_box: str = " {} " ;
393412
394413 if not canvas_empty {
395- # Canvas non-empty: request confirmation
396- confirmed: bool = _request_confirm(
397- self ._provider,
398- " Replace canvas content with new diagram?"
414+ # Canvas non-empty: request confirmation via chat
415+ confirm_result: str = _chat_confirm(
416+ self .ws,
417+ self .event_loop,
418+ self .session_id,
419+ " Canvas is not empty. **Approve** to clear it and render the new diagram, or **Deny** to place it beside the existing content."
399420 );
400- if not confirmed {
401- log(" compose_diagram: user declined to replace canvas " );
402- self ._session_context.canvas_summary = " non-empty, user kept " ;
403- _send_ws( self .ws, { " type " : " message " , " sessionId " : self .session_id, " content " : " Diagram creation cancelled. Canvas kept as-is. " }, self .event_loop);
404- _send_ws( self .ws, { " type " : " done " , " sessionId " : self .session_id}, self .event_loop );
405- return ;
421+ if confirm_result == " ok " {
422+ log(" compose_diagram: user approved replace" );
423+ # canvas_mode stays "replace"
424+ } else {
425+ log( " compose_diagram: user denied replace, switching to additive mode " );
426+ canvas_mode = " additive " ;
406427 }
407- log(" compose_diagram: user confirmed replacement" );
408428 }
409429
410430 if canvas_mode == " additive" {
411431 try {
412- existing_elements_json = self ._provider.call_tool(" get_canvas_elements" , {});
432+ elements_result: str = self ._provider.call_tool(" get_canvas_elements" , {});
433+ # call_tool returns JSON string; parse and re-serialize to ensure proper format
434+ parsed_els: dict = json.loads(elements_result);
435+ # Extract just the elements array if it's wrapped in a dict
436+ if isinstance (parsed_els, dict ) and " elements" in parsed_els {
437+ existing_elements_json = json.dumps(parsed_els[" elements" ]);
438+ } else {
439+ existing_elements_json = elements_result;
440+ }
441+ log(f " compose_diagram: fetched { len (parsed_els) if isinstance (parsed_els, list ) else len (parsed_els.get(' elements' , []))} existing elements " );
413442 } except Exception as e {
414443 log(f " compose_diagram: error fetching elements: { e} " );
444+ existing_elements_json = " []" ;
445+ }
446+ try {
447+ bb_result: str = self ._provider.call_tool(" get_bounding_box" , {});
448+ # call_tool returns JSON string; verify it's valid JSON
449+ parsed_bb: dict = json.loads(bb_result);
450+ existing_bounding_box = json.dumps(parsed_bb);
451+ log(f " compose_diagram: bounding box = { existing_bounding_box} " );
452+ } except Exception as e {
453+ log(f " compose_diagram: error fetching bounding box: { e} " );
454+ existing_bounding_box = " {} " ;
415455 }
416456 }
417457
@@ -426,7 +466,8 @@ walker AgentOrchestrator {
426466 self ._session_context.conversation_log,
427467 self ._session_context.diagram_blueprint,
428468 canvas_mode,
429- existing_elements_json
469+ existing_elements_json,
470+ existing_bounding_box
430471 );
431472 log(f " compose_diagram: response = { response[:80 ]} " );
432473
0 commit comments