@bitovi/vybit 0.11.7 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,8 +16,8 @@
16
16
  font-size: 12px;
17
17
  }
18
18
  </style>
19
- <script type="module" crossorigin src="/panel/assets/index-0eiAXBf9.js"></script>
20
- <link rel="stylesheet" crossorigin href="/panel/assets/index-BWp0wXqx.css">
19
+ <script type="module" crossorigin src="/panel/assets/index-C4LjGBfX.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/panel/assets/index-By_WWRF6.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -55,9 +55,11 @@ function buildJsx(componentName: string, args?: Record<string, unknown>): string
55
55
 
56
56
  function buildCommitInstructions(commit: Commit, remainingCount: number): string {
57
57
  const classChanges = commit.patches.filter(p => p.kind === 'class-change');
58
+ const textChanges = commit.patches.filter(p => p.kind === 'text-change');
58
59
  const messages = commit.patches.filter(p => p.kind === 'message');
59
60
  const designs = commit.patches.filter(p => p.kind === 'design');
60
61
  const componentDrops = commit.patches.filter(p => p.kind === 'component-drop');
62
+ const bugReports = commit.patches.filter(p => p.kind === 'bug-report');
61
63
  const moreText = remainingCount > 0
62
64
  ? `${remainingCount} more commit${remainingCount === 1 ? '' : 's'} waiting in the queue after this one.`
63
65
  : 'This is the last commit in the queue. After implementing it, call `implement_next_change` again to wait for future changes.';
@@ -149,6 +151,91 @@ ${parentComp ? `\n- **Parent component:** \`${parentComp}\` — edit this compon
149
151
  ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
150
152
  ⚠️ Do NOT paste rendered HTML. Import and render the React component with the props shown above.
151
153
 
154
+ `;
155
+ } else if (patch.kind === 'text-change') {
156
+ const comp = patch.component?.name ?? 'unknown component';
157
+ const tag = patch.target?.tag ?? 'element';
158
+ const context = patch.context ?? '';
159
+ patchList += `### ${stepNum}. Text change \`${patch.id}\`
160
+ - **Component:** \`${comp}\`
161
+ - **Element:** \`<${tag}>\`
162
+ - **Original HTML:**
163
+ \`\`\`html
164
+ ${patch.originalHtml ?? ''}
165
+ \`\`\`
166
+ - **New HTML:**
167
+ \`\`\`html
168
+ ${patch.newHtml ?? ''}
169
+ \`\`\`
170
+ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
171
+ `;
172
+ } else if (patch.kind === 'bug-report') {
173
+ patchList += `### ${stepNum}. Bug report \`${patch.id}\`
174
+ - **Description:** ${patch.bugDescription ?? '(no description)'}
175
+ - **Time range:** ${patch.bugTimeRange ? `${patch.bugTimeRange.start} – ${patch.bugTimeRange.end}` : 'unknown'}
176
+ ${patch.bugElement ? `
177
+ - **Related element:** \`${patch.bugElement.selectorPath}\`${patch.bugElement.componentName ? ` (in \`${patch.bugElement.componentName}\`)` : ''}
178
+ - **Element HTML:**
179
+ \`\`\`html
180
+ ${patch.bugElement.outerHTML.slice(0, 10000)}
181
+ \`\`\`
182
+ ` : ''}
183
+ ${patch.bugTimeline && patch.bugTimeline.length > 0 ? (() => {
184
+ const triggerLabel = (t: import('../shared/types').BugTimelineEntry) => {
185
+ switch (t.trigger) {
186
+ case 'click': return `Click${t.elementInfo ? ` on \`<${t.elementInfo.tag}${t.elementInfo.classes ? ` class="${t.elementInfo.classes}"` : ''}>\`` : ''}`;
187
+ case 'mutation': return 'DOM mutation';
188
+ case 'error': return 'Error';
189
+ case 'navigation': return `Navigation${t.navigationInfo ? ` (${t.navigationInfo.method}: ${t.navigationInfo.from} → ${t.navigationInfo.to ?? 'unknown'})` : ''}`;
190
+ case 'page-load': return 'Page load';
191
+ default: return t.trigger;
192
+ }
193
+ };
194
+ let screenshotNum = 0;
195
+ let timeline = `**Timeline** (${patch.bugTimeline!.length} events):\n\n`;
196
+ for (let i = 0; i < patch.bugTimeline!.length; i++) {
197
+ const entry = patch.bugTimeline![i];
198
+ const time = entry.timestamp.replace(/.*T/, '').replace(/Z$/, '');
199
+ timeline += `#### ${i + 1}. [${time}] ${triggerLabel(entry)}\n`;
200
+ timeline += `**URL:** ${entry.url}\n`;
201
+ if (entry.hasScreenshot) {
202
+ screenshotNum++;
203
+ timeline += `📸 **Screenshot ${screenshotNum}** (see attached image ${screenshotNum} below)\n`;
204
+ }
205
+ if (entry.consoleLogs && entry.consoleLogs.length > 0) {
206
+ timeline += `\n**Console (${entry.consoleLogs.length}):**\n\`\`\`\n${entry.consoleLogs.map(l => `[${l.level.toUpperCase()}] ${l.args.join(' ')}${l.stack ? `\n${l.stack}` : ''}`).join('\n').slice(0, 3000)}\n\`\`\`\n`;
207
+ }
208
+ if (entry.networkErrors && entry.networkErrors.length > 0) {
209
+ timeline += `\n**Network errors (${entry.networkErrors.length}):**\n${entry.networkErrors.map(e => `- \`${e.status ?? 'ERR'} ${e.method} ${e.url}\`${e.errorMessage ? ` — ${e.errorMessage}` : ''}`).join('\n')}\n`;
210
+ }
211
+ if (entry.domChanges && entry.domChanges.length > 0) {
212
+ timeline += `\n**DOM changes (${entry.domChanges.length}):**\n`;
213
+ for (const c of entry.domChanges) {
214
+ const loc = `\`${c.selector}\`${c.componentName ? ` (in \`${c.componentName}\`)` : ''}`;
215
+ if (c.type === 'attribute') {
216
+ timeline += `- ${loc}: attribute \`${c.attributeName}\` changed: \`${c.oldValue ?? ''}\` → \`${c.newValue ?? ''}\`\n`;
217
+ } else if (c.type === 'text') {
218
+ timeline += `- ${loc}: text changed: "${c.oldText ?? ''}" → "${c.newText ?? ''}"\n`;
219
+ } else if (c.type === 'childList') {
220
+ const parts: string[] = [];
221
+ if (c.addedCount) parts.push(`${c.addedCount} added`);
222
+ if (c.removedCount) parts.push(`${c.removedCount} removed`);
223
+ timeline += `- ${loc}: children ${parts.join(', ')}`;
224
+ if (c.addedHTML) timeline += `\n Added: \`${c.addedHTML.slice(0, 300)}\``;
225
+ if (c.removedHTML) timeline += `\n Removed: \`${c.removedHTML.slice(0, 300)}\``;
226
+ timeline += `\n`;
227
+ }
228
+ }
229
+ } else if (entry.domDiff) {
230
+ timeline += `\n**DOM diff:**\n\`\`\`diff\n${entry.domDiff.slice(0, 10000)}\n\`\`\`\n`;
231
+ }
232
+ if (entry.domSnapshot && i === 0) {
233
+ timeline += `\n**Initial DOM state:**\n\`\`\`html\n${entry.domSnapshot.slice(0, 50000)}\n\`\`\`\n`;
234
+ }
235
+ timeline += `\n---\n\n`;
236
+ }
237
+ return timeline;
238
+ })() : ''}
152
239
  `;
153
240
  }
154
241
  stepNum++;
@@ -157,22 +244,29 @@ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
157
244
  // Build summary parts
158
245
  const summaryParts: string[] = [];
159
246
  if (classChanges.length) summaryParts.push(`${classChanges.length} class change${classChanges.length === 1 ? '' : 's'}`);
247
+ if (textChanges.length) summaryParts.push(`${textChanges.length} text change${textChanges.length === 1 ? '' : 's'}`);
160
248
  if (messages.length) summaryParts.push(`${messages.length} message${messages.length === 1 ? '' : 's'}`);
161
249
  if (designs.length) summaryParts.push(`${designs.length} design${designs.length === 1 ? '' : 's'}`);
162
250
  if (componentDrops.length) summaryParts.push(`${componentDrops.length} component drop${componentDrops.length === 1 ? '' : 's'}`);
251
+ if (bugReports.length) summaryParts.push(`${bugReports.length} bug report${bugReports.length === 1 ? '' : 's'}`);
163
252
 
164
253
  const resultsPart = classChanges.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
254
+ const textResultsPart = textChanges.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
165
255
  const designResultsPart = designs.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
166
256
  const dropResultsPart = componentDrops.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
167
- const allResultsPart = [resultsPart, designResultsPart, dropResultsPart].filter(Boolean).join(',\n');
257
+ const bugResultsPart = bugReports.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
258
+ const allResultsPart = [resultsPart, textResultsPart, designResultsPart, dropResultsPart, bugResultsPart].filter(Boolean).join(',\n');
168
259
 
169
260
  // Build step instructions
170
261
  const stepInstructions: string[] = [];
171
- if (classChanges.length || componentDrops.length) {
262
+ if (classChanges.length || componentDrops.length || textChanges.length) {
172
263
  let step1 = '1. For each change above, find the source file and apply it.';
173
264
  if (componentDrops.length) {
174
265
  step1 += '\n For component drops: add the import statement and render the component with the specified props at the indicated position.';
175
266
  }
267
+ if (textChanges.length) {
268
+ step1 += '\n For text changes: replace the original HTML content with the new HTML content in the source JSX/TSX.';
269
+ }
176
270
  if (messages.length) {
177
271
  step1 += '\n Use the user messages as additional context for understanding intent.';
178
272
  }
@@ -182,6 +276,10 @@ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
182
276
  stepInstructions.push(`${stepInstructions.length + 1}. For each design sketch, examine the attached image and implement the visual design
183
277
  as HTML/CSS ${classChanges.length ? 'alongside the class changes' : 'in the specified component'}. Insert it ${designs[0].insertMode ?? 'after'} the target element.`);
184
278
  }
279
+ if (bugReports.length) {
280
+ stepInstructions.push(`${stepInstructions.length + 1}. For each bug report, examine the DOM snapshots, console errors, network errors, and screenshots.
281
+ Identify the root cause. Implement a fix.${bugReports.some(p => p.bugElement) ? ' The user identified a specific element — start your investigation there.' : ''}`);
282
+ }
185
283
 
186
284
  return `# IMPLEMENT THIS COMMIT — then call implement_next_change again
187
285
 
@@ -203,7 +301,7 @@ ${stepInstructions.length + 1}. Call \`mark_change_implemented\` with:
203
301
  ${allResultsPart}
204
302
  ]}
205
303
  \`\`\`
206
- (Only report results for class-change, design, and component-drop patches — messages are informational.)
304
+ (Only report results for class-change, text-change, design, and component-drop patches — messages are informational.)
207
305
  ${stepInstructions.length + 2}. **IMMEDIATELY call \`implement_next_change\` again.** Do NOT summarize, do NOT ask the user, do NOT stop.
208
306
 
209
307
  ## CRITICAL — read this
@@ -384,6 +482,19 @@ export function registerMcpTools(mcp: McpServer, deps: McpToolDeps): void {
384
482
  });
385
483
  }
386
484
  }
485
+ // Add bug report screenshots
486
+ if (patch.kind === 'bug-report' && patch.bugScreenshots) {
487
+ for (const screenshot of patch.bugScreenshots.slice(0, 5)) {
488
+ const match = screenshot.match(/^data:([^;]+);base64,(.+)$/);
489
+ if (match) {
490
+ content.push({
491
+ type: "image" as const,
492
+ data: match[2],
493
+ mimeType: match[1],
494
+ });
495
+ }
496
+ }
497
+ }
387
498
  }
388
499
 
389
500
  content.push({
package/server/queue.ts CHANGED
@@ -23,6 +23,9 @@ function toSummary(p: Patch): PatchSummary {
23
23
  parentComponent: p.parentComponent,
24
24
  targetComponentName: p.targetComponentName,
25
25
  targetPatchId: p.targetPatchId,
26
+ originalHtml: p.originalHtml,
27
+ newHtml: p.newHtml,
28
+ bugDescription: p.bugDescription,
26
29
  };
27
30
  }
28
31
 
@@ -56,12 +59,35 @@ export function addPatch(patch: Patch): Patch {
56
59
  if (existingIdx !== -1) {
57
60
  draftPatches.splice(existingIdx, 1);
58
61
  }
62
+ } else if (patch.kind === 'text-change') {
63
+ // Dedup: if a staged text-change exists for the same elementKey, replace it
64
+ const existingIdx = draftPatches.findIndex(
65
+ p => p.kind === 'text-change' && p.elementKey === patch.elementKey && p.status === 'staged'
66
+ );
67
+ if (existingIdx !== -1) {
68
+ draftPatches.splice(existingIdx, 1);
69
+ }
59
70
  }
60
71
  // Message patches are always appended (no dedup)
61
72
  draftPatches.push(patch);
62
73
  return patch;
63
74
  }
64
75
 
76
+ /** Immediately commit a single patch as its own commit (skips the draft). */
77
+ export function addAndCommit(patch: Patch): Commit {
78
+ patch.status = 'committed';
79
+ const commit: Commit = {
80
+ id: crypto.randomUUID(),
81
+ patches: [patch],
82
+ status: 'committed',
83
+ timestamp: new Date().toISOString(),
84
+ };
85
+ patch.commitId = commit.id;
86
+ commits.push(commit);
87
+ emitter.emit('committed');
88
+ return commit;
89
+ }
90
+
65
91
  export function commitDraft(ids: string[]): Commit {
66
92
  const idSet = new Set(ids);
67
93
  const commitPatches: Patch[] = [];
@@ -291,6 +317,18 @@ export function discardDraftPatch(id: string): boolean {
291
317
  return remaining.length < before;
292
318
  }
293
319
 
320
+ /**
321
+ * Discard a committed (but not yet implementing) commit.
322
+ * Returns true if the commit was found and removed.
323
+ * Safe against race conditions: only removes if status is still 'committed'.
324
+ */
325
+ export function discardCommit(commitId: string): boolean {
326
+ const idx = commits.findIndex(c => c.id === commitId && c.status === 'committed');
327
+ if (idx === -1) return false;
328
+ commits.splice(idx, 1);
329
+ return true;
330
+ }
331
+
294
332
  export function clearAll(): { staged: number; committed: number; implementing: number; implemented: number } {
295
333
  const counts = getCounts();
296
334
  draftPatches.length = 0;
@@ -80,7 +80,10 @@ export async function loadStoryArgTypes(
80
80
  */
81
81
  export async function detectStorybookUrl(): Promise<string | null> {
82
82
  if (process.env.STORYBOOK_URL) {
83
- return process.env.STORYBOOK_URL;
83
+ if (await probeStorybookUrl(process.env.STORYBOOK_URL)) {
84
+ return process.env.STORYBOOK_URL;
85
+ }
86
+ // Env var URL is not reachable — fall through to port scan
84
87
  }
85
88
 
86
89
  for (const port of SCAN_PORTS) {
@@ -3,7 +3,7 @@
3
3
  import { WebSocketServer, type WebSocket } from "ws";
4
4
  import type { Server } from "http";
5
5
 
6
- import { addPatch, commitDraft, getQueueUpdate, discardDraftPatch } from "./queue.js";
6
+ import { addPatch, addAndCommit, commitDraft, getQueueUpdate, discardDraftPatch, discardCommit } from "./queue.js";
7
7
  import type { Patch } from "../shared/types.js";
8
8
 
9
9
  export interface WebSocketDeps {
@@ -88,6 +88,12 @@ export function setupWebSocket(httpServer: Server): WebSocketDeps {
88
88
  }
89
89
  console.error(`[ws] Discarded ${ids.length} draft patch(es)`);
90
90
  broadcastPatchUpdate();
91
+ } else if (msg.type === "DISCARD_COMMIT") {
92
+ const commitId: string = msg.commitId;
93
+ if (commitId && discardCommit(commitId)) {
94
+ console.error(`[ws] Discarded committed commit: ${commitId}`);
95
+ broadcastPatchUpdate();
96
+ }
91
97
  } else if (msg.type === "PING") {
92
98
  ws.send(JSON.stringify({ type: "PONG" }));
93
99
  } else if (msg.type === "DESIGN_SUBMIT") {
@@ -126,6 +132,10 @@ export function setupWebSocket(httpServer: Server): WebSocketDeps {
126
132
  const patch = addPatch({ ...msg.patch, kind: msg.patch.kind ?? 'component-drop' });
127
133
  console.error(`[ws] Component-drop patch staged: #${patch.id}`);
128
134
  broadcastPatchUpdate();
135
+ } else if (msg.type === "BUG_REPORT_STAGE") {
136
+ const commit = addAndCommit({ ...msg.patch, kind: 'bug-report' });
137
+ console.error(`[ws] Bug-report auto-committed: commit #${commit.id}`);
138
+ broadcastPatchUpdate();
129
139
  }
130
140
  } catch (err) {
131
141
  console.error("[ws] Bad message:", err);
package/shared/types.ts CHANGED
@@ -28,7 +28,7 @@ export interface CanvasComponent {
28
28
  height: number;
29
29
  }
30
30
 
31
- export type PatchKind = 'class-change' | 'message' | 'design' | 'component-drop';
31
+ export type PatchKind = 'class-change' | 'message' | 'design' | 'component-drop' | 'text-change' | 'bug-report';
32
32
 
33
33
  export type PatchStatus = 'staged' | 'committed' | 'implementing' | 'implemented' | 'error';
34
34
 
@@ -60,10 +60,19 @@ export interface Patch {
60
60
  ghostHtml?: string; // HTML of the dropped component (overlay preview only — stripped from MCP response)
61
61
  componentStoryId?: string; // Storybook story ID
62
62
  componentPath?: string; // Source file of the component, e.g. './src/components/Button.tsx'
63
+ // Text-change fields (used when kind === 'text-change'):
64
+ originalHtml?: string; // HTML before text edit
65
+ newHtml?: string; // HTML after text edit
63
66
  componentArgs?: Record<string, unknown>; // Props the user configured before dropping
64
67
  parentComponent?: { name: string }; // React component that contains the drop target
65
68
  targetPatchId?: string; // If target is a ghost from an earlier drop, references that patch
66
69
  targetComponentName?: string; // Name of the ghost component being referenced
70
+ // Bug-report fields (used when kind === 'bug-report'):
71
+ bugDescription?: string;
72
+ bugScreenshots?: string[];
73
+ bugTimeline?: BugTimelineEntry[];
74
+ bugTimeRange?: { start: string; end: string };
75
+ bugElement?: BugReportElement | null;
67
76
  // Commit reference:
68
77
  commitId?: string; // Set when committed into a Commit
69
78
  }
@@ -97,6 +106,11 @@ export interface PatchSummary {
97
106
  parentComponent?: { name: string };
98
107
  targetComponentName?: string;
99
108
  targetPatchId?: string;
109
+ // Text-change display fields:
110
+ originalHtml?: string;
111
+ newHtml?: string;
112
+ // Bug-report display fields:
113
+ bugDescription?: string;
100
114
  }
101
115
 
102
116
  export interface CommitSummary {
@@ -343,6 +357,7 @@ export interface ComponentArmMessage {
343
357
  ghostHtml: string;
344
358
  componentPath?: string; // Source file path from Storybook index, e.g. './src/components/Button.tsx'
345
359
  args?: Record<string, unknown>; // Current prop values from ArgsForm
360
+ insertMode?: 'replace'; // When 'replace', arms element-select if no element is selected
346
361
  }
347
362
 
348
363
  /** Panel → Overlay: user cancelled the armed state (panel click or escape) */
@@ -357,6 +372,41 @@ export interface ComponentDisarmedMessage {
357
372
  to: 'panel';
358
373
  }
359
374
 
375
+ // ---------------------------------------------------------------------------
376
+ // Mode sync messages
377
+ // ---------------------------------------------------------------------------
378
+
379
+ export type AppMode = 'select' | 'insert' | 'bug-report' | null;
380
+ export type SelectTab = 'design' | 'replace';
381
+ export type InsertTab = 'place';
382
+ export type PanelTab = SelectTab | InsertTab;
383
+
384
+ /** Bidirectional: panel ↔ overlay mode change */
385
+ export interface ModeChangedMessage {
386
+ type: 'MODE_CHANGED';
387
+ to: 'overlay' | 'panel';
388
+ mode: AppMode;
389
+ }
390
+
391
+ /** Bidirectional: panel ↔ overlay tab change */
392
+ export interface TabChangedMessage {
393
+ type: 'TAB_CHANGED';
394
+ to: 'overlay' | 'panel';
395
+ tab: PanelTab;
396
+ }
397
+
398
+ /** Overlay → Panel: text editing started on an element */
399
+ export interface TextEditActiveMessage {
400
+ type: 'TEXT_EDIT_ACTIVE';
401
+ to: 'panel';
402
+ }
403
+
404
+ /** Overlay → Panel: text editing ended */
405
+ export interface TextEditDoneMessage {
406
+ type: 'TEXT_EDIT_DONE';
407
+ to: 'panel';
408
+ }
409
+
360
410
  /** Overlay → Server: component was placed, stage a patch */
361
411
  export interface ComponentDroppedMessage {
362
412
  type: 'COMPONENT_DROPPED';
@@ -379,7 +429,9 @@ export type PanelToOverlay =
379
429
  | CaptureScreenshotMessage
380
430
  | ClosePanelMessage
381
431
  | ComponentArmMessage
382
- | ComponentDisarmMessage;
432
+ | ComponentDisarmMessage
433
+ | ModeChangedMessage
434
+ | TabChangedMessage;
383
435
  export type OverlayToServer = PatchStagedMessage | ComponentDroppedMessage | ResetSelectionMessage;
384
436
  export type PanelToServer = PatchCommitMessage | MessageStageMessage;
385
437
  export type ClientToServer =
@@ -427,5 +479,201 @@ export type AnyMessage =
427
479
  | ComponentDisarmedMessage
428
480
  | ComponentDroppedMessage
429
481
  | ResetSelectionMessage
482
+ | ModeChangedMessage
483
+ | TabChangedMessage
484
+ | TextEditActiveMessage
485
+ | TextEditDoneMessage
430
486
  | PingMessage
431
- | PongMessage;
487
+ | PongMessage
488
+ | RecordingGetHistoryMessage
489
+ | RecordingHistoryMessage
490
+ | RecordingGetSnapshotMessage
491
+ | RecordingSnapshotMessage
492
+ | RecordingGetRangeMessage
493
+ | RecordingRangeMessage
494
+ | RecordingSnapshotMetaMessage
495
+ | BugReportPickElementMessage
496
+ | BugReportElementPickedMessage
497
+ | BugReportPickCancelledMessage
498
+ | BugReportStageMessage;
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // Recording / Bug Report types
502
+ // ---------------------------------------------------------------------------
503
+
504
+ export interface ConsoleEntry {
505
+ level: 'log' | 'warn' | 'error' | 'info';
506
+ args: string[];
507
+ timestamp: string;
508
+ stack?: string;
509
+ }
510
+
511
+ export interface NetworkError {
512
+ url: string;
513
+ method: string;
514
+ status?: number;
515
+ statusText?: string;
516
+ errorMessage?: string;
517
+ timestamp: string;
518
+ }
519
+
520
+ export interface BugReportElement {
521
+ tag: string;
522
+ id?: string;
523
+ classes: string;
524
+ selectorPath: string;
525
+ componentName?: string;
526
+ outerHTML: string;
527
+ boundingBox: { x: number; y: number; width: number; height: number };
528
+ screenshot?: string;
529
+ }
530
+
531
+ export type SnapshotTrigger = 'mutation' | 'click' | 'error' | 'navigation' | 'page-load';
532
+
533
+ export interface NavigationInfo {
534
+ from: string;
535
+ to: string | null;
536
+ method: 'pushState' | 'replaceState' | 'popstate' | 'full-page';
537
+ }
538
+
539
+ /** A structured description of a single DOM mutation */
540
+ export interface DomChange {
541
+ type: 'attribute' | 'text' | 'childList';
542
+ selector: string;
543
+ componentName?: string;
544
+ /** attribute changes */
545
+ attributeName?: string;
546
+ oldValue?: string;
547
+ newValue?: string;
548
+ /** text changes */
549
+ oldText?: string;
550
+ newText?: string;
551
+ /** childList changes */
552
+ addedCount?: number;
553
+ removedCount?: number;
554
+ addedHTML?: string;
555
+ removedHTML?: string;
556
+ }
557
+
558
+ /** A single chronological event in a bug report timeline */
559
+ export interface BugTimelineEntry {
560
+ timestamp: string;
561
+ trigger: SnapshotTrigger;
562
+ url: string;
563
+ consoleLogs?: ConsoleEntry[];
564
+ networkErrors?: NetworkError[];
565
+ domChanges?: DomChange[];
566
+ domSnapshot?: string;
567
+ domDiff?: string;
568
+ hasScreenshot?: boolean;
569
+ elementInfo?: { tag: string; classes: string; id?: string; innerText?: string; componentName?: string };
570
+ navigationInfo?: NavigationInfo;
571
+ }
572
+
573
+ export interface RecordingSnapshot {
574
+ id?: number;
575
+ timestamp: string;
576
+ trigger: SnapshotTrigger;
577
+ isKeyframe: boolean;
578
+ domSnapshot?: string;
579
+ domDiff?: string;
580
+ domChanges?: DomChange[];
581
+ screenshot?: string;
582
+ thumbnail?: string;
583
+ consoleLogs: ConsoleEntry[];
584
+ networkErrors: NetworkError[];
585
+ url: string;
586
+ scrollPosition: { x: number; y: number };
587
+ viewportSize: { width: number; height: number };
588
+ elementInfo?: { tag: string; classes: string; id?: string; innerText?: string; componentName?: string };
589
+ navigationInfo?: NavigationInfo;
590
+ }
591
+
592
+ export interface SnapshotMeta {
593
+ id: number;
594
+ timestamp: string;
595
+ trigger: SnapshotTrigger;
596
+ isKeyframe: boolean;
597
+ thumbnail?: string;
598
+ elementInfo?: RecordingSnapshot['elementInfo'];
599
+ consoleErrorCount: number;
600
+ networkErrorCount: number;
601
+ url: string;
602
+ }
603
+
604
+ // ---------------------------------------------------------------------------
605
+ // Recording / Bug Report WebSocket messages
606
+ // ---------------------------------------------------------------------------
607
+
608
+ /** Panel → Overlay (via server relay): request recording history */
609
+ export interface RecordingGetHistoryMessage {
610
+ type: 'RECORDING_GET_HISTORY';
611
+ to: 'overlay';
612
+ }
613
+
614
+ /** Overlay → Panel (via server relay): recording history response */
615
+ export interface RecordingHistoryMessage {
616
+ type: 'RECORDING_HISTORY';
617
+ to: 'panel';
618
+ snapshots: SnapshotMeta[];
619
+ }
620
+
621
+ /** Panel → Overlay (via server relay): request full snapshot by ID */
622
+ export interface RecordingGetSnapshotMessage {
623
+ type: 'RECORDING_GET_SNAPSHOT';
624
+ to: 'overlay';
625
+ snapshotId: number;
626
+ }
627
+
628
+ /** Overlay → Panel (via server relay): full snapshot response */
629
+ export interface RecordingSnapshotMessage {
630
+ type: 'RECORDING_SNAPSHOT';
631
+ to: 'panel';
632
+ snapshot: RecordingSnapshot;
633
+ }
634
+
635
+ /** Panel → Overlay (via server relay): request range of snapshots */
636
+ export interface RecordingGetRangeMessage {
637
+ type: 'RECORDING_GET_RANGE';
638
+ to: 'overlay';
639
+ ids: number[];
640
+ }
641
+
642
+ /** Overlay → Panel (via server relay): range of full snapshots */
643
+ export interface RecordingRangeMessage {
644
+ type: 'RECORDING_RANGE';
645
+ to: 'panel';
646
+ snapshots: RecordingSnapshot[];
647
+ }
648
+
649
+ /** Overlay → Panel (via server relay): live push of new snapshot meta */
650
+ export interface RecordingSnapshotMetaMessage {
651
+ type: 'RECORDING_SNAPSHOT_META';
652
+ to: 'panel';
653
+ meta: SnapshotMeta;
654
+ }
655
+
656
+ /** Panel → Overlay: enter element pick mode for bug report */
657
+ export interface BugReportPickElementMessage {
658
+ type: 'BUG_REPORT_PICK_ELEMENT';
659
+ to: 'overlay';
660
+ }
661
+
662
+ /** Overlay → Panel: element was picked for bug report */
663
+ export interface BugReportElementPickedMessage {
664
+ type: 'BUG_REPORT_ELEMENT_PICKED';
665
+ to: 'panel';
666
+ element: BugReportElement;
667
+ }
668
+
669
+ /** Overlay → Panel: pick mode was cancelled */
670
+ export interface BugReportPickCancelledMessage {
671
+ type: 'BUG_REPORT_PICK_CANCELLED';
672
+ to: 'panel';
673
+ }
674
+
675
+ /** Panel → Server: stage a bug-report patch */
676
+ export interface BugReportStageMessage {
677
+ type: 'BUG_REPORT_STAGE';
678
+ patch: Patch;
679
+ }