From 2fd87b2d141b8006423dc8d825b78bdf4788d727 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Sat, 20 Dec 2025 14:02:11 -0600 Subject: [PATCH] Fix entity connections and add clear diagram tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connection fixes: - Add chatResolveEntity event to resolve labels to entity IDs - Update connectEntities to resolve from/to labels before creating connection - Auto-generate connection labels as "{from label} to {to label}" Clear diagram tool: - Add clear_diagram tool with confirmation requirement - Claude prompts user for confirmation before executing - Clears all entities from diagram and resets session - Syncs empty entity list to server after clearing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/diagram/diagramManager.ts | 37 ++++++++++ src/react/services/diagramAI.ts | 27 +++++++- src/react/services/entityBridge.ts | 107 +++++++++++++++++++++++++++-- src/react/types/chatTypes.ts | 7 +- 4 files changed, 171 insertions(+), 7 deletions(-) diff --git a/src/diagram/diagramManager.ts b/src/diagram/diagramManager.ts index a2f5f75..5b53fc0 100644 --- a/src/diagram/diagramManager.ts +++ b/src/diagram/diagramManager.ts @@ -179,6 +179,43 @@ export class DiagramManager { document.dispatchEvent(responseEvent); }); + // Resolve entity label/ID to actual entity ID and label + document.addEventListener('chatResolveEntity', (event: CustomEvent) => { + const {target, requestId} = event.detail; + this._logger.debug('chatResolveEntity', target); + const entity = this.findEntityByIdOrLabel(target); + const responseEvent = new CustomEvent('chatResolveEntityResponse', { + detail: { + requestId, + target, + entityId: entity?.id || null, + entityLabel: entity?.text || null, + found: !!entity + }, + bubbles: true + }); + document.dispatchEvent(responseEvent); + }); + + // Clear all entities from the diagram + document.addEventListener('chatClearDiagram', () => { + this._logger.debug('chatClearDiagram - removing all entities'); + const entitiesToRemove = Array.from(this._diagramObjects.keys()); + for (const id of entitiesToRemove) { + const diagramObject = this._diagramObjects.get(id); + if (diagramObject) { + const entity = diagramObject.diagramEntity; + diagramObject.dispose(); + this._diagramObjects.delete(id); + this.onDiagramEventObservable.notifyObservers({ + type: DiagramEventType.REMOVE, + entity: entity + }, DiagramEventObserverMask.TO_DB); + } + } + this._logger.debug(`Cleared ${entitiesToRemove.length} entities`); + }); + this._logger.debug("DiagramManager constructed"); } diff --git a/src/react/services/diagramAI.ts b/src/react/services/diagramAI.ts index e2fe47a..88858ef 100644 --- a/src/react/services/diagramAI.ts +++ b/src/react/services/diagramAI.ts @@ -1,5 +1,5 @@ import {ChatMessage, CreateSessionResponse, DiagramSession, DiagramToolCall, SessionEntity, SyncEntitiesResponse, ToolResult} from "../types/chatTypes"; -import {connectEntities, createEntity, listEntities, modifyEntity, removeEntity} from "./entityBridge"; +import {clearDiagram, connectEntities, createEntity, listEntities, modifyEntity, removeEntity} from "./entityBridge"; import {v4 as uuidv4} from 'uuid'; // Session management @@ -204,6 +204,20 @@ const TOOLS = [ }, required: ["target"] } + }, + { + name: "clear_diagram", + description: "DESTRUCTIVE: Permanently delete ALL entities from the diagram and clear the session. This cannot be undone. IMPORTANT: Before calling this tool, you MUST first ask the user to confirm by saying something like 'Are you sure you want to clear the entire diagram? This will permanently delete all entities and cannot be undone.' Only call this tool with confirmed=true AFTER the user explicitly confirms (e.g., says 'yes', 'confirm', 'do it', etc.).", + input_schema: { + type: "object", + properties: { + confirmed: { + type: "boolean", + description: "Must be true to execute. Only set to true after user has explicitly confirmed the deletion." + } + }, + required: ["confirmed"] + } } ]; @@ -232,13 +246,22 @@ async function executeToolCall(toolCall: DiagramToolCall): Promise { case 'create_entity': return createEntity(toolCall.input); case 'connect_entities': - return connectEntities(toolCall.input); + return await connectEntities(toolCall.input); case 'remove_entity': return removeEntity(toolCall.input); case 'modify_entity': return modifyEntity(toolCall.input); case 'list_entities': return await listEntities(); + case 'clear_diagram': + const result = await clearDiagram(toolCall.input); + // If successful, also clear the session (history and entities) + if (result.success && currentSessionId) { + await clearSessionHistory(); + // Sync empty entity list to clear server-side entity cache + await syncEntitiesToSession([]); + } + return result; default: return { toolName: 'unknown', diff --git a/src/react/services/entityBridge.ts b/src/react/services/entityBridge.ts index d11d482..4b7263a 100644 --- a/src/react/services/entityBridge.ts +++ b/src/react/services/entityBridge.ts @@ -1,5 +1,6 @@ import {DiagramEntity, DiagramEntityType, DiagramTemplates} from "../../diagram/types/diagramEntity"; import { + ClearDiagramParams, COLOR_NAME_TO_HEX, ConnectEntitiesParams, CreateEntityParams, @@ -22,6 +23,42 @@ function resolveColor(color?: string): string { return '#0000ff'; } +interface ResolvedEntity { + id: string | null; + label: string | null; +} + +/** + * Resolve an entity label or ID to actual entity ID and label + */ +function resolveEntity(target: string): Promise { + return new Promise((resolve) => { + const requestId = 'req-' + Date.now() + '-' + Math.random(); + + const responseHandler = (e: CustomEvent) => { + if (e.detail.requestId !== requestId) return; + document.removeEventListener('chatResolveEntityResponse', responseHandler as EventListener); + resolve({ + id: e.detail.entityId, + label: e.detail.entityLabel + }); + }; + + document.addEventListener('chatResolveEntityResponse', responseHandler as EventListener); + + const event = new CustomEvent('chatResolveEntity', { + detail: {target, requestId}, + bubbles: true + }); + document.dispatchEvent(event); + + setTimeout(() => { + document.removeEventListener('chatResolveEntityResponse', responseHandler as EventListener); + resolve({id: null, label: null}); + }, 5000); + }); +} + export function createEntity(params: CreateEntityParams): ToolResult { const id = 'id' + uuidv4(); const template = SHAPE_TO_TEMPLATE[params.shape]; @@ -53,17 +90,43 @@ export function createEntity(params: CreateEntityParams): ToolResult { }; } -export function connectEntities(params: ConnectEntitiesParams): ToolResult { +export async function connectEntities(params: ConnectEntitiesParams): Promise { + // Resolve labels to actual entity IDs and get their labels + const fromEntity = await resolveEntity(params.from); + const toEntity = await resolveEntity(params.to); + + if (!fromEntity.id) { + return { + toolName: 'connect_entities', + success: false, + message: `Could not find entity "${params.from}"` + }; + } + + if (!toEntity.id) { + return { + toolName: 'connect_entities', + success: false, + message: `Could not find entity "${params.to}"` + }; + } + const id = 'id' + uuidv4(); const color = resolveColor(params.color); + // Generate default label from entity labels: "{from label} to {to label}" + const fromLabel = fromEntity.label || params.from; + const toLabel = toEntity.label || params.to; + const connectionLabel = `${fromLabel} to ${toLabel}`; + const entity: DiagramEntity = { id, template: DiagramTemplates.CONNECTION, type: DiagramEntityType.ENTITY, color, - from: params.from, - to: params.to, + text: connectionLabel, + from: fromEntity.id, + to: toEntity.id, }; const event = new CustomEvent('chatCreateEntity', { @@ -75,7 +138,7 @@ export function connectEntities(params: ConnectEntitiesParams): ToolResult { return { toolName: 'connect_entities', success: true, - message: `Connected "${params.from}" to "${params.to}"`, + message: `Connected "${fromLabel}" to "${toLabel}"`, entityId: id }; } @@ -195,3 +258,39 @@ export function getEntitiesForSync(): Promise { + if (!params.confirmed) { + return { + toolName: 'clear_diagram', + success: false, + message: 'Clearing the diagram requires confirmation. Please ask the user to confirm before proceeding.' + }; + } + + // Get all entities first + const entities = await getEntitiesForSync(); + + if (entities.length === 0) { + return { + toolName: 'clear_diagram', + success: true, + message: 'The diagram is already empty.' + }; + } + + // Dispatch clear event to remove all entities at once + const event = new CustomEvent('chatClearDiagram', { + bubbles: true + }); + document.dispatchEvent(event); + + return { + toolName: 'clear_diagram', + success: true, + message: `Cleared ${entities.length} entities from the diagram.` + }; +} diff --git a/src/react/types/chatTypes.ts b/src/react/types/chatTypes.ts index f10b1d1..c8163c9 100644 --- a/src/react/types/chatTypes.ts +++ b/src/react/types/chatTypes.ts @@ -42,12 +42,17 @@ export interface ModifyEntityParams { position?: { x: number; y: number; z: number }; } +export interface ClearDiagramParams { + confirmed: boolean; +} + export type DiagramToolCall = | { name: 'create_entity'; input: CreateEntityParams } | { name: 'connect_entities'; input: ConnectEntitiesParams } | { name: 'remove_entity'; input: RemoveEntityParams } | { name: 'modify_entity'; input: ModifyEntityParams } - | { name: 'list_entities'; input: Record }; + | { name: 'list_entities'; input: Record } + | { name: 'clear_diagram'; input: ClearDiagramParams }; export const SHAPE_TO_TEMPLATE: Record = { box: DiagramTemplates.BOX,