1/** 2 * Conflict Resolver Overlay 3 * UI for resolving file conflicts when multiple agents edit the same file 4 */ 5 6export class ConflictResolver { 7 constructor(canvas, fileNode) { 8 this.canvas = canvas; 9 this.fileNode = fileNode; 10 this.overlay = null; 11 this.fabricObjects = []; 12 } 13 14 /** 15 * Show conflict resolution UI 16 */ 17 show() { 18 if (this.overlay) return; // Already showing 19 20 const canvasWidth = this.canvas.width; 21 const canvasHeight = this.canvas.height; 22 23 // Semi-transparent backdrop 24 const backdrop = new fabric.Rect({ 25 left: 0, 26 top: 0, 27 width: canvasWidth, 28 height: canvasHeight, 29 fill: 'rgba(0, 0, 0, 0.5)', 30 selectable: false, 31 evented: true 32 }); 33 34 // Click backdrop to close 35 backdrop.on('mousedown', () => this.hide()); 36 this.fabricObjects.push(backdrop); 37 this.canvas.add(backdrop); 38 39 // Modal dialog 40 const modalWidth = 600; 41 const modalHeight = 400; 42 const modalX = (canvasWidth - modalWidth) / 2; 43 const modalY = (canvasHeight - modalHeight) / 2; 44 45 // Modal background 46 const modal = new fabric.Rect({ 47 left: modalX, 48 top: modalY, 49 width: modalWidth, 50 height: modalHeight, 51 fill: '#ffffff', 52 rx: 12, 53 ry: 12, 54 shadow: new fabric.Shadow({ 55 color: 'rgba(0, 0, 0, 0.3)', 56 blur: 30, 57 offsetX: 0, 58 offsetY: 10 59 }), 60 selectable: false, 61 evented: false 62 }); 63 this.fabricObjects.push(modal); 64 this.canvas.add(modal); 65 66 // Header 67 this.addHeader(modalX, modalY, modalWidth); 68 69 // Conflict info 70 this.addConflictInfo(modalX, modalY + 60, modalWidth); 71 72 // Agent list 73 this.addAgentList(modalX, modalY + 120, modalWidth); 74 75 // Resolution buttons 76 this.addResolutionButtons(modalX, modalY + modalHeight - 80, modalWidth); 77 78 this.canvas.renderAll(); 79 } 80 81 /** 82 * Add header section 83 */ 84 addHeader(x, y, width) { 85 // Title 86 const title = new fabric.Text('â ï¸ Conflict Detected', { 87 left: x + 24, 88 top: y + 20, 89 fontSize: 20, 90 fontFamily: 'system-ui, -apple-system, sans-serif', 91 fontWeight: '600', 92 fill: '#e74c3c', 93 selectable: false, 94 evented: false, 95 textBaseline: 'alphabetic' 96 }); 97 this.fabricObjects.push(title); 98 this.canvas.add(title); 99 100 // Close button (X) 101 const closeButton = new fabric.Circle({ 102 left: x + width - 40, 103 top: y + 20, 104 radius: 16, 105 fill: '#ecf0f1', 106 selectable: false, 107 evented: true, 108 hoverCursor: 'pointer' 109 }); 110 111 const closeX = new fabric.Text('â', { 112 left: x + width - 36, 113 top: y + 21, 114 fontSize: 18, 115 fontFamily: 'system-ui, -apple-system, sans-serif', 116 fill: '#7f8c8d', 117 selectable: false, 118 evented: false, 119 textBaseline: 'alphabetic' 120 }); 121 122 closeButton.on('mousedown', () => this.hide()); 123 124 this.fabricObjects.push(closeButton, closeX); 125 this.canvas.add(closeButton); 126 this.canvas.add(closeX); 127 128 // Divider line 129 const divider = new fabric.Line([x + 24, y + 55, x + width - 24, y + 55], { 130 stroke: '#ecf0f1', 131 strokeWidth: 1, 132 selectable: false, 133 evented: false 134 }); 135 this.fabricObjects.push(divider); 136 this.canvas.add(divider); 137 } 138 139 /** 140 * Add conflict information 141 */ 142 addConflictInfo(x, y, width) { 143 const infoText = `File: ${this.fileNode.fileName}\nStatus: ${this.fileNode.status}\nModified by: ${this.fileNode.modifiedBy.length} agents`; 144 145 const info = new fabric.Text(infoText, { 146 left: x + 24, 147 top: y, 148 fontSize: 14, 149 fontFamily: 'system-ui, -apple-system, sans-serif', 150 fill: '#2c3e50', 151 lineHeight: 1.6, 152 selectable: false, 153 evented: false, 154 textBaseline: 'alphabetic' 155 }); 156 this.fabricObjects.push(info); 157 this.canvas.add(info); 158 } 159 160 /** 161 * Add list of agents with conflict 162 */ 163 addAgentList(x, y, width) { 164 const listTitle = new fabric.Text('Conflicting Agents:', { 165 left: x + 24, 166 top: y, 167 fontSize: 15, 168 fontFamily: 'system-ui, -apple-system, sans-serif', 169 fontWeight: '600', 170 fill: '#2c3e50', 171 selectable: false, 172 evented: false, 173 textBaseline: 'alphabetic' 174 }); 175 this.fabricObjects.push(listTitle); 176 this.canvas.add(listTitle); 177 178 // List each agent 179 this.fileNode.modifiedBy.forEach((agentId, index) => { 180 const agentY = y + 35 + (index * 60); 181 this.addAgentCard(x + 24, agentY, width - 48, agentId, index); 182 }); 183 } 184 185 /** 186 * Add individual agent card 187 */ 188 addAgentCard(x, y, width, agentId, index) { 189 // Card background 190 const card = new fabric.Rect({ 191 left: x, 192 top: y, 193 width: width, 194 height: 50, 195 fill: '#f8f9fa', 196 rx: 8, 197 ry: 8, 198 stroke: this.fileNode.getAgentColor(agentId), 199 strokeWidth: 2, 200 selectable: false, 201 evented: false 202 }); 203 this.fabricObjects.push(card); 204 this.canvas.add(card); 205 206 // Agent avatar
207 const avatar = new fabric.Circle({ 208 left: x + 12, 209 top: y + 13, 210 radius: 12, 211 fill: this.fileNode.getAgentColor(agentId), 212 stroke: '#ffffff', 213 strokeWidth: 2, 214 selectable: false, 215 evented: false 216 }); 217 this.fabricObjects.push(avatar); 218 this.canvas.add(avatar); 219 220 // Agent ID text 221 const agentText = new fabric.Text(`Agent ${index + 1}: ${agentId.substring(0, 12)}...`, { 222 left: x + 40, 223 top: y + 10, 224 fontSize: 13, 225 fontFamily: 'system-ui, -apple-system, sans-serif', 226 fontWeight: '500', 227 fill: '#2c3e50', 228 selectable: false, 229 evented: false, 230 textBaseline: 'alphabetic' 231 }); 232 this.fabricObjects.push(agentText); 233 this.canvas.add(agentText); 234 235 // "Keep" button 236 const keepButton = this.createButton( 237 x + width - 80, 238 y + 10, 239 70, 240 30, 241 'Keep', 242 this.fileNode.getAgentColor(agentId), 243 () => this.resolveConflict('keep', agentId) 244 ); 245 this.fabricObjects.push(...keepButton); 246 keepButton.forEach(obj => this.canvas.add(obj)); 247 } 248 249 /** 250 * Add resolution action buttons 251 */ 252 addResolutionButtons(x, y, width) { 253 const buttonY = y + 20; 254 const buttonWidth = 140; 255 const spacing = 16; 256 257 // "Merge All" button 258 const mergeButton = this.createButton( 259 x + 24, 260 buttonY, 261 buttonWidth, 262 40, 263 'Merge All', 264 '#3498db', 265 () => this.resolveConflict('merge') 266 ); 267 this.fabricObjects.push(...mergeButton); 268 mergeButton.forEach(obj => this.canvas.add(obj)); 269 270 // "Manual Review" button 271 const reviewButton = this.createButton( 272 x + 24 + buttonWidth + spacing, 273 buttonY, 274 buttonWidth, 275 40, 276 'Manual Review', 277 '#9b59b6', 278 () => this.resolveConflict('manual') 279 ); 280 this.fabricObjects.push(...reviewButton); 281 reviewButton.forEach(obj => this.canvas.add(obj)); 282 283 // "Cancel" button 284 const cancelButton = this.createButton( 285 x + width - buttonWidth - 24, 286 buttonY, 287 buttonWidth, 288 40, 289 'Cancel', 290 '#95a5a6', 291 () => this.hide() 292 ); 293 this.fabricObjects.push(...cancelButton); 294 cancelButton.forEach(obj => this.canvas.add(obj)); 295 } 296 297 /** 298 * Create a button with text 299 */ 300 createButton(x, y, width, height, text, color, onClick) { 301 const button = new fabric.Rect({ 302 left: x, 303 top: y, 304 width: width, 305 height: height, 306 fill: color, 307 rx: 8, 308 ry: 8, 309 selectable: false, 310 evented: true, 311 hoverCursor: 'pointer', 312 shadow: new fabric.Shadow({ 313 color: 'rgba(0,0,0,0.2)', 314 blur: 5, 315 offsetX: 0, 316 offsetY: 2 317 }) 318 }); 319 320 const buttonText = new fabric.Text(text, { 321 left: x + width / 2, 322 top: y + height / 2, 323 originX: 'center', 324 originY: 'center', 325 fontSize: 14, 326 fontFamily: 'system-ui, -apple-system, sans-serif', 327 fontWeight: '600', 328 fill: '#ffffff', 329 selectable: false, 330 evented: false, 331 textBaseline: 'alphabetic' 332 }); 333 334 // Hover effect 335 button.on('mouseover', () => { 336 button.set('opacity', 0.8); 337 this.canvas.renderAll(); 338 }); 339 340 button.on('mouseout', () => { 341 button.set('opacity', 1); 342 this.canvas.renderAll(); 343 }); 344 345 // Click handler 346 button.on('mousedown', onClick); 347 348 return [button, buttonText]; 349 } 350 351 /** 352 * Resolve conflict with chosen strategy 353 */ 354 resolveConflict(strategy, agentId = null) { 355 console.log(`Resolving conflict with strategy: ${strategy}`, agentId); 356 357 // Emit event for canvas to handle 358 this.canvas.fire('conflict:resolved', { 359 fileNode: this.fileNode, 360 fileName: this.fileNode.fileName, 361 strategy: strategy, 362 agentId: agentId 363 }); 364 365 // Update file node status 366 if (strategy === 'keep') { 367 // Keep only selected agent's changes 368 this.fileNode.modifiedBy = [agentId]; 369 this.fileNode.hasConflict = false; 370 this.fileNode.status = 'modified'; 371 } else if (strategy === 'merge') { 372 // Merge all changes (keep all agents for now) 373 this.fileNode.hasConflict = false;
374 this.fileNode.status = 'modified'; 375 } else if (strategy === 'manual') { 376 // Open in external editor for manual resolution 377 // Keep conflict status for now 378 console.log('Opening manual review...'); 379 } 380 381 // Re-render file node 382 if (strategy !== 'manual') { 383 const canvas = this.fileNode.fabricObject.canvas; 384 canvas.remove(this.fileNode.fabricObject); 385 this.fileNode.render(canvas); 386 canvas.renderAll(); 387 } 388 389 // Close overlay 390 this.hide(); 391 } 392 393 /** 394 * Hide conflict resolver 395 */ 396 hide() { 397 // Remove all fabric objects 398 this.fabricObjects.forEach(obj => { 399 this.canvas.remove(obj); 400 }); 401 this.fabricObjects = []; 402 this.overlay = null; 403 this.canvas.renderAll(); 404 } 405 406 /** 407 * Check if currently showing 408 */ 409 isShowing() { 410 return this.overlay !== null; 411 } 412}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.