PageSourceSearch

https://tyga.ai/js/canvas/overlays/conflictResolver.js

js tyga.ai collected 2026-10-05 15:10:58 UTC 10,058 bytes, 412 lines download raw bytes

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.