Skip to content

Commit a85d194

Browse files
committed
fix: Refactor canvas element handling in orchestrator and analyzer; update ChatPanel model options
1 parent 89323d6 commit a85d194

6 files changed

Lines changed: 32 additions & 44 deletions

File tree

agent/orchestrator.jac

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import from .subagents {
4747
DiagramComposer,
4848
DiagramEditor
4949
}
50-
import from .subagents.analyzer { _set_analyst_provider }
5150
import from .subagents.synthesizer { _set_composer_provider }
5251
import from .subagents.refiner { _set_editor_provider }
5352
import from .llm_factory { create_llm }
@@ -167,6 +166,7 @@ def _send_command(action: str, data: dict | None = None, session_id: str = "", t
167166
}
168167

169168
payload: str = json.dumps({
169+
"role": "controller",
170170
"requestId": request_id,
171171
"sessionId": session_id,
172172
"action": action,
@@ -258,7 +258,7 @@ walker AgentOrchestrator {
258258
can init_context with Root entry {
259259
# Find or create SessionContext for this session
260260
self._provider = CanvasToolProvider(
261-
send_fn=lambda (action: str, data: dict[str, Any]) -> Any {_send_command(action, data, self.session_id);}
261+
send_fn=lambda (action: str, data: dict[str, Any]) -> Any {return _send_command(action, data, self.session_id);}
262262
);
263263

264264
# Find or create session context (persists across turns)
@@ -431,6 +431,7 @@ walker AgentOrchestrator {
431431
log(f"compose_diagram: response = {response[:80]}");
432432

433433
self._session_context.clear_diagram_blueprint();
434+
self._session_context.canvas_summary = "non-empty";
434435
self._session_context.append_turn("composer", response);
435436
_send_ws(self.ws, {"type": "message", "sessionId": self.session_id, "content": response}, self.event_loop);
436437
_send_ws(self.ws, {"type": "done", "sessionId": self.session_id}, self.event_loop);
@@ -443,11 +444,20 @@ walker AgentOrchestrator {
443444
if _diagram_analyst is None { return; }
444445

445446
log("explain_canvas: starting DiagramAnalyst");
446-
_set_analyst_provider(self._provider);
447+
448+
# Pre-fetch canvas elements directly (same pattern as _check_canvas_empty)
449+
canvas_elements_json: str = '{"elements":[],"total":0}';
450+
try {
451+
canvas_elements_json = self._provider.call_tool("get_canvas_elements", {});
452+
} except Exception as e {
453+
log(f"explain_canvas: error fetching elements: {e}");
454+
}
455+
log(f"explain_canvas: canvas_elements_json = {canvas_elements_json[:120]}");
447456

448457
response: str = _diagram_analyst.explain(
449458
self.message,
450-
self._session_context.conversation_log
459+
self._session_context.conversation_log,
460+
canvas_elements_json
451461
);
452462
log(f"explain_canvas: response = {response[:80]}");
453463

agent/subagents/analyzer.jac

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""DiagramAnalyst — explains/describes the diagram on the canvas.
22
3-
Fetches canvas elements via get_canvas_elements tool and provides analysis.
3+
Canvas elements are pre-fetched by the orchestrator and passed as canvas_elements_json.
44
Read-only agent: no modification tools.
55
"""
66

@@ -12,12 +12,11 @@ node DiagramAnalyst {
1212
has api_key: str = "";
1313
has llm: Any = None;
1414

15-
def explain(message: str, conversation_log: str) -> str
15+
def explain(message: str, conversation_log: str, canvas_elements_json: str) -> str
1616
by self.llm(
17-
tools=[use_canvas_tool],
18-
max_react_iterations=2,
17+
tools=[],
1918
incl_info={
20-
"available_canvas_tools": [t for t in _analyst_provider.list_tools() if t["name"] in ANALYST_TOOLS],
19+
"canvas_elements_json": canvas_elements_json,
2120
"conversation": conversation_log
2221
}
2322
);
@@ -26,10 +25,7 @@ node DiagramAnalyst {
2625
sem DiagramAnalyst.explain = """
2726
Explain the JaSketch canvas diagram to the user.
2827
29-
Workflow:
30-
1. Call use_canvas_tool("get_canvas_elements", "{}") to fetch the current diagram
31-
2. Analyze the elements to understand the diagram structure
32-
3. Explain to the user what the diagram shows, its purpose, key components, and relationships
28+
Canvas data is already provided in `canvas_elements_json` from context — parse it directly.
3329
3430
Element Types and Key Fields (runtime format):
3531
- rectangle / circle / diamond: x, y, width, height, shapeText (label inside shape), color, fillColor, boundElements (list of connected arrow IDs)
@@ -41,39 +37,16 @@ Element Types and Key Fields (runtime format):
4137
- freehand: x, y, points (free-drawn path)
4238
4339
Your Task:
44-
1. Fetch canvas elements via use_canvas_tool("get_canvas_elements", "{}")
40+
1. Parse canvas_elements_json from incl_info to extract the "elements" array
4541
2. Identify shapes (by type + shapeText labels) and connections (arrows via startBinding/endBinding elementIds)
4642
3. Understand the overall diagram structure: what it represents, its purpose, layout
4743
4. Explain clearly: what diagram this is, what it shows, key components, and how they relate
4844
45+
If canvas_elements_json is empty or has no elements, explain that the canvas is currently empty.
46+
4947
Read-only: explain only, do NOT attempt to modify the canvas.
5048
5149
User message: {{message}}
5250
5351
Respond now.
5452
""";
55-
56-
# ── Tool binding (populated by orchestrator before each call) ──────────────────
57-
58-
glob ANALYST_TOOLS: list[str] = ["get_canvas_elements"];
59-
60-
glob _analyst_provider: Any = None;
61-
62-
def use_canvas_tool(name: str, arguments: str) -> str {
63-
args: dict = {};
64-
try {
65-
if isinstance(arguments, dict) { args = arguments; }
66-
elif arguments { args = json.loads(arguments); }
67-
} except Exception { args = {}; }
68-
return _analyst_provider.call_tool(name, args);
69-
}
70-
sem use_canvas_tool = "Call a canvas tool by name. Only use tools listed in available_canvas_tools from context.";
71-
sem use_canvas_tool.name = "Tool name — must exactly match one of the names in available_canvas_tools.";
72-
sem use_canvas_tool.arguments = "JSON object string matching the inputSchema of the named tool. Build from the schema in available_canvas_tools.";
73-
74-
# ── Provider injection (called by orchestrator) ────────────────────────────────
75-
76-
def _set_analyst_provider(provider: Any) -> None {
77-
global _analyst_provider;
78-
_analyst_provider = provider;
79-
}

components/layout/ChatPanel.cl.jac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def:pub ChatPanel(props: dict) -> JsxElement {
222222
modelOptions = [
223223
{"id": "gpt-4o", "label": "GPT-4o"},
224224
{"id": "gpt-4o-mini", "label": "GPT-4o Mini"},
225-
{"id": "gpt-5.4-mini", "label": "GPT-5.4 Mini"},
225+
{"id": "gpt-5-mini", "label": "GPT-5 Mini"},
226226
{"id": "claude-sonnet-4-20250514", "label": "Claude Sonnet"},
227227
{"id": "claude-haiku-4-20250414", "label": "Claude Haiku"},
228228
{"id": "gemini/gemini-2.0-flash", "label": "Gemini Flash"},

hooks/useElements.cl.jac

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ def:pub useElements() -> dict {
55
historyRef = useRef([]);
66
historyIndexRef = useRef(-1);
77
loadedRef = useRef(False);
8+
elementsRef = useRef([]);
89

910
STORAGE_KEY = "jasketch_elements";
1011

1112
def setElementsInternal(newElements: list) -> None {
1213
elements = newElements;
14+
elementsRef.current = newElements;
1315
# Strip non-serializable imageObj before saving to localStorage
1416
toStore = [
1517
el.type == "image" and el.imageObj and {
@@ -36,6 +38,7 @@ def:pub useElements() -> dict {
3638
mi = mi + 1;
3739
}
3840
elements = parsed;
41+
elementsRef.current = parsed;
3942
pushHistory(parsed);
4043
}
4144
}
@@ -194,6 +197,7 @@ def:pub useElements() -> dict {
194197

195198
return {
196199
"elements": elements,
200+
"elementsRef": elementsRef,
197201
"setElements": setElementsWithHistory,
198202
"addElement": addElement,
199203
"addMultipleElements": addMultipleElements,

hooks/useWebSocket.cl.jac

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def:pub useWebSocket(elementsHook: dict, viewportHook: dict, canvasRef: any, can
3535
}
3636

3737
def sendResponse(ws: any, requestId: str, success: bool, data: any) -> None {
38-
msg = {"requestId": requestId, "success": success, "data": data};
38+
msg = {"role": "canvas", "requestId": requestId, "success": success, "data": data};
3939
ws.send(JSON.stringify(msg));
4040
}
4141

@@ -70,7 +70,8 @@ def:pub useWebSocket(elementsHook: dict, viewportHook: dict, canvasRef: any, can
7070
}
7171
sendResponse(ws, requestId, True, {"ids": addedIds, "count": items.length, "action": "created"});
7272
} elif action == "getElements" {
73-
elems = hook.elements;
73+
# Use elementsRef for synchronous read (avoids stale React state snapshot)
74+
elems = (hook.elementsRef and hook.elementsRef.current) or hook.elements;
7475
result = [];
7576
for el in elems {
7677
if el and (not data.type or el.type == data.type) {
@@ -208,8 +209,8 @@ def:pub useWebSocket(elementsHook: dict, viewportHook: dict, canvasRef: any, can
208209
sendError(ws, requestId, "Element not found: " + findTargetId);
209210
}
210211
} elif action == "setElements" {
211-
hook.clearElements();
212212
newElements = data.elements or [];
213+
hook.clearElements();
213214
hook.addMultipleElements(newElements);
214215

215216
# Auto-fit viewport: compute bounding box of all elements, then zoom to fit with 60px padding

mcp-server/jasketch_mcp/canvas_tool_provider.jac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ obj CanvasToolProvider {
572572
"""Fetch all canvas elements with defaults stripped for token efficiency."""
573573
def _get_canvas_elements(args: dict) -> str {
574574
result: Any = self._send("getElements", {});
575-
if not result or not result.get("elements") {
575+
if not result or "elements" not in result {
576576
return json.dumps({"elements": [], "total": 0}, separators=(',', ':'));
577577
}
578578
# Strip {id, element} wrapper → plain element objects

0 commit comments

Comments
 (0)