elasticdash-test 0.1.16 → 0.1.17-alpha-2

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.
Files changed (86) hide show
  1. package/dist/cloud-client.d.ts +34 -0
  2. package/dist/cloud-client.d.ts.map +1 -0
  3. package/dist/cloud-client.js +103 -0
  4. package/dist/cloud-client.js.map +1 -0
  5. package/dist/dashboard-server.d.ts.map +1 -1
  6. package/dist/dashboard-server.js +425 -90
  7. package/dist/dashboard-server.js.map +1 -1
  8. package/dist/evaluators/determinism.d.ts +3 -0
  9. package/dist/evaluators/determinism.d.ts.map +1 -0
  10. package/dist/evaluators/determinism.js +116 -0
  11. package/dist/evaluators/determinism.js.map +1 -0
  12. package/dist/evaluators/index.d.ts +4 -0
  13. package/dist/evaluators/index.d.ts.map +1 -0
  14. package/dist/evaluators/index.js +61 -0
  15. package/dist/evaluators/index.js.map +1 -0
  16. package/dist/evaluators/latency-budget.d.ts +3 -0
  17. package/dist/evaluators/latency-budget.d.ts.map +1 -0
  18. package/dist/evaluators/latency-budget.js +45 -0
  19. package/dist/evaluators/latency-budget.js.map +1 -0
  20. package/dist/evaluators/llm-judge.d.ts +3 -0
  21. package/dist/evaluators/llm-judge.d.ts.map +1 -0
  22. package/dist/evaluators/llm-judge.js +122 -0
  23. package/dist/evaluators/llm-judge.js.map +1 -0
  24. package/dist/evaluators/output-contains.d.ts +3 -0
  25. package/dist/evaluators/output-contains.d.ts.map +1 -0
  26. package/dist/evaluators/output-contains.js +52 -0
  27. package/dist/evaluators/output-contains.js.map +1 -0
  28. package/dist/evaluators/output-schema.d.ts +3 -0
  29. package/dist/evaluators/output-schema.d.ts.map +1 -0
  30. package/dist/evaluators/output-schema.js +58 -0
  31. package/dist/evaluators/output-schema.js.map +1 -0
  32. package/dist/evaluators/token-budget.d.ts +3 -0
  33. package/dist/evaluators/token-budget.d.ts.map +1 -0
  34. package/dist/evaluators/token-budget.js +45 -0
  35. package/dist/evaluators/token-budget.js.map +1 -0
  36. package/dist/evaluators/types.d.ts +103 -0
  37. package/dist/evaluators/types.d.ts.map +1 -0
  38. package/dist/evaluators/types.js +6 -0
  39. package/dist/evaluators/types.js.map +1 -0
  40. package/dist/html/dashboard.html +359 -73
  41. package/dist/index.cjs +253 -14
  42. package/dist/index.d.ts +2 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/interceptors/telemetry-push.d.ts +40 -0
  47. package/dist/interceptors/telemetry-push.d.ts.map +1 -1
  48. package/dist/interceptors/telemetry-push.js +129 -6
  49. package/dist/interceptors/telemetry-push.js.map +1 -1
  50. package/dist/interceptors/tool.d.ts.map +1 -1
  51. package/dist/interceptors/tool.js +8 -1
  52. package/dist/interceptors/tool.js.map +1 -1
  53. package/dist/interceptors/workflow-ai.d.ts.map +1 -1
  54. package/dist/interceptors/workflow-ai.js +21 -6
  55. package/dist/interceptors/workflow-ai.js.map +1 -1
  56. package/dist/internals/mock-resolver.d.ts +70 -2
  57. package/dist/internals/mock-resolver.d.ts.map +1 -1
  58. package/dist/internals/mock-resolver.js +157 -6
  59. package/dist/internals/mock-resolver.js.map +1 -1
  60. package/dist/test-group/cli.d.ts +8 -0
  61. package/dist/test-group/cli.d.ts.map +1 -0
  62. package/dist/test-group/cli.js +162 -0
  63. package/dist/test-group/cli.js.map +1 -0
  64. package/dist/test-group/git-context.d.ts +3 -0
  65. package/dist/test-group/git-context.d.ts.map +1 -0
  66. package/dist/test-group/git-context.js +59 -0
  67. package/dist/test-group/git-context.js.map +1 -0
  68. package/dist/test-group/reporter.d.ts +4 -0
  69. package/dist/test-group/reporter.d.ts.map +1 -0
  70. package/dist/test-group/reporter.js +54 -0
  71. package/dist/test-group/reporter.js.map +1 -0
  72. package/dist/test-group/runner.d.ts +18 -0
  73. package/dist/test-group/runner.d.ts.map +1 -0
  74. package/dist/test-group/runner.js +234 -0
  75. package/dist/test-group/runner.js.map +1 -0
  76. package/dist/workflow-runner-worker.js +7 -1
  77. package/dist/workflow-runner-worker.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/dashboard-server.ts +101 -28
  80. package/src/html/dashboard.html +359 -73
  81. package/src/index.ts +2 -2
  82. package/src/interceptors/telemetry-push.ts +159 -7
  83. package/src/interceptors/tool.ts +9 -1
  84. package/src/interceptors/workflow-ai.ts +22 -6
  85. package/src/internals/mock-resolver.ts +190 -7
  86. package/src/workflow-runner-worker.ts +11 -3
@@ -5,6 +5,30 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>ElasticDash Dashboard</title>
7
7
  <style>
8
+ /* Help icon tooltip */
9
+ .help-icon-wrap { position: relative; display: inline-flex; align-items: center; }
10
+ .help-icon {
11
+ display: inline-flex; align-items: center; justify-content: center;
12
+ width: 16px; height: 16px; border-radius: 50%;
13
+ background: #e0e0e0; color: #666; font-size: 11px; font-weight: bold;
14
+ cursor: help; vertical-align: middle; margin-left: 4px;
15
+ user-select: none;
16
+ }
17
+ .help-tooltip {
18
+ display: none; position: absolute; left: 50%; bottom: calc(100% + 8px);
19
+ transform: translateX(-50%);
20
+ background: #333; color: #fff; font-size: 12px; font-weight: 400;
21
+ line-height: 1.4; padding: 8px 12px; border-radius: 6px;
22
+ width: 280px; white-space: normal; z-index: 10000;
23
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
24
+ pointer-events: auto;
25
+ }
26
+ .help-tooltip::after {
27
+ content: ''; position: absolute; top: 100%; left: 50%;
28
+ transform: translateX(-50%);
29
+ border: 6px solid transparent; border-top-color: #333;
30
+ }
31
+ .help-icon-wrap:hover .help-tooltip { display: block; }
8
32
  /* Ensure first cell in observation-table never overflows parent */
9
33
  .observation-table td:first-child {
10
34
  max-width: 120px;
@@ -314,7 +338,7 @@
314
338
  window._toolMockConfig = {}; // { toolName: { mode: 'live'|'mock-all'|'mock-specific', callIndices: [], mockData: {} } }
315
339
 
316
340
  // ---- Prompt Mock Config State ----
317
- // { [originalSystemPrompt]: newSystemPrompt } only keys where user enabled the override
341
+ // { [originalSystemPrompt]: { mode, replacement, callIndices? } }
318
342
  window._promptMockConfig = {};
319
343
 
320
344
  function getToolsFromTrace() {
@@ -322,7 +346,7 @@
322
346
  const toolCalls = {};
323
347
  currentObservations.forEach(function(obs, i) {
324
348
  const isToolByType = obs.type === 'TOOL';
325
- const isToolByName = typeof obs.name === 'string' && obs.name.startsWith('tool-');
349
+ const isToolByName = typeof obs.name === 'string' && (obs.name.startsWith('tool-') || obs.name.startsWith('tool:'));
326
350
  if (!isToolByType && !isToolByName) return;
327
351
  const name = isToolByName && obs.type !== 'TOOL'
328
352
  ? obs.name.slice(5)
@@ -408,14 +432,15 @@
408
432
  return '<div style="color:#999;font-size:13px;padding:6px 0;">No tools detected.</div>';
409
433
  }
410
434
 
411
- let html = '<div style="max-height:260px;overflow-y:auto;border:1px solid #e0e0e0;border-radius:6px;">';
412
- html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
435
+ let html = '<div style="border:1px solid #e0e0e0;border-radius:6px;height:100%;display:flex;flex-direction:column;overflow:hidden;">';
436
+ html += '<table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;">';
413
437
  html += '<thead><tr style="background:#f5f5f5;">';
414
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Tool</th>';
415
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls in Trace</th>';
416
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mock Mode</th>';
417
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Details</th>';
418
- html += '</tr></thead><tbody>';
438
+ html += '<th style="width:30%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Tool</th>';
439
+ html += '<th style="width:15%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls</th>';
440
+ html += '<th style="width:20%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mode</th>';
441
+ html += '<th style="width:35%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Details</th>';
442
+ html += '</tr></thead></table>';
443
+ html += '<div style="flex:1;overflow-y:auto;"><table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;"><tbody>';
419
444
 
420
445
  toolNames.forEach(function(name) {
421
446
  const calls = traceTools[name] || [];
@@ -424,10 +449,10 @@
424
449
  const nameStyle = inTrace ? '' : 'color:#999;';
425
450
 
426
451
  html += '<tr class="tool-mock-row" data-tool-name="' + esc(name) + '" style="border-bottom:1px solid #f0f0f0;">';
427
- html += '<td style="padding:6px 10px;font-family:Monaco,monospace;' + nameStyle + '">' + esc(name) + (inTrace ? '' : ' <span style="font-size:10px;color:#aaa;">(not in trace)</span>') + '</td>';
428
- html += '<td style="padding:6px 10px;">' + calls.length + '</td>';
429
- html += '<td style="padding:6px 10px;">';
430
- html += '<select class="tool-mock-mode" style="font-size:12px;padding:2px 4px;" onchange="window.onToolMockModeChange(\'' + esc(name) + '\', this.value)">';
452
+ html += '<td style="width:30%;padding:6px 10px;font-family:Monaco,monospace;overflow:hidden;text-overflow:ellipsis;' + nameStyle + '">' + esc(name) + (inTrace ? '' : ' <span style="font-size:10px;color:#aaa;">(not in trace)</span>') + '</td>';
453
+ html += '<td style="width:15%;padding:6px 10px;">' + calls.length + '</td>';
454
+ html += '<td style="width:20%;padding:6px 10px;">';
455
+ html += '<select class="tool-mock-mode" style="font-size:12px;padding:2px 4px;width: 100%;" onchange="window.onToolMockModeChange(\'' + esc(name) + '\', this.value)">';
431
456
  html += '<option value="live"' + (existing.mode === 'live' ? ' selected' : '') + '>Live</option>';
432
457
  html += '<option value="mock-all"' + (existing.mode === 'mock-all' ? ' selected' : '') + '>Mock All Calls</option>';
433
458
  if (calls.length > 0) {
@@ -437,7 +462,7 @@
437
462
  html += '</td>';
438
463
 
439
464
  // Details column: per-call checkboxes + mock data inputs
440
- html += '<td style="padding:6px 10px;">';
465
+ html += '<td style="width:35%;padding:6px 10px;">';
441
466
  if (existing.mode === 'mock-all') {
442
467
  let defaultData = (existing.mockData && existing.mockData[0] !== undefined) ? JSON.stringify(existing.mockData[0]) : (calls.length > 0 ? JSON.stringify(calls[0].output) : '');
443
468
  defaultData = convert(defaultData);
@@ -468,7 +493,7 @@
468
493
  html += '</tr>';
469
494
  });
470
495
 
471
- html += '</tbody></table></div>';
496
+ html += '</tbody></table></div></div>';
472
497
  return html;
473
498
  }
474
499
 
@@ -476,9 +501,10 @@
476
501
 
477
502
  /** Extract the system prompt string from an LLM call input object or JSON string. */
478
503
  function extractSystemPromptFromInput(input) {
479
- // Input may arrive as a JSON-encoded string (e.g. from Langfuse traces)
480
- if (typeof input === 'string') {
481
- try { input = JSON.parse(input); } catch(e) { return null; }
504
+ // Input may arrive as a JSON-encoded string (e.g. from Langfuse traces).
505
+ // Unwrap until we get a non-string value.
506
+ while (typeof input === 'string') {
507
+ try { input = JSON.parse(input); } catch(e) { break; }
482
508
  }
483
509
  if (!input || typeof input !== 'object') return null;
484
510
  // Anthropic style: { system: "...", messages: [...] }
@@ -500,19 +526,24 @@
500
526
 
501
527
  /**
502
528
  * Returns an array of unique system prompts observed across all GENERATION observations.
503
- * Each entry: { systemPrompt, modelName, count, rowIndex }
529
+ * Each entry: { systemPrompt, modelName, calls: [{obsIndex, callNumber}], count, rowIndex }
530
+ * callNumber is the 1-indexed occurrence of this prompt across GENERATION observations.
504
531
  */
505
532
  function getSystemPromptsFromTrace() {
506
- var seen = []; // [{ systemPrompt, modelName, count }]
533
+ var seen = []; // [{ systemPrompt, modelName, calls, count }]
507
534
  var seenMap = {}; // systemPrompt -> index in seen
508
- currentObservations.forEach(function(obs) {
535
+ var callCounters = {}; // systemPrompt -> running count
536
+ currentObservations.forEach(function(obs, obsIndex) {
509
537
  if (obs.type !== 'GENERATION') return;
510
538
  var sp = extractSystemPromptFromInput(obs.input);
511
539
  if (!sp) return;
540
+ callCounters[sp] = (callCounters[sp] || 0) + 1;
541
+ var callNumber = callCounters[sp];
512
542
  if (seenMap[sp] === undefined) {
513
543
  seenMap[sp] = seen.length;
514
- seen.push({ systemPrompt: sp, modelName: obs.model || obs.name || '(unknown)', count: 0 });
544
+ seen.push({ systemPrompt: sp, modelName: obs.model || obs.name || '(unknown)', calls: [], count: 0 });
515
545
  }
546
+ seen[seenMap[sp]].calls.push({ obsIndex: obsIndex, callNumber: callNumber });
516
547
  seen[seenMap[sp]].count++;
517
548
  });
518
549
  return seen.map(function(e, i) { return Object.assign({}, e, { rowIndex: i }); });
@@ -524,82 +555,145 @@
524
555
  return '<div style="color:#999;font-size:13px;padding:6px 0;">No system prompts detected in trace. Only AI calls with a system prompt can be mocked here.</div>';
525
556
  }
526
557
 
527
- let html = '<div style="max-height:360px;overflow-y:auto;border:1px solid #e0e0e0;border-radius:6px;">';
528
- html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
558
+ let html = '<div style="border:1px solid #e0e0e0;border-radius:6px;height:100%;display:flex;flex-direction:column;overflow:hidden;">';
559
+ html += '<table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;">';
529
560
  html += '<thead><tr style="background:#f5f5f5;">';
530
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;width:24px;"></th>';
531
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Model</th>';
532
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Uses</th>';
533
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">System Prompt (override applies to all calls using this prompt)</th>';
534
- html += '</tr></thead><tbody>';
561
+ html += '<th style="width:25%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">System Prompt</th>';
562
+ html += '<th style="width:10%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls</th>';
563
+ html += '<th style="width:20%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mode</th>';
564
+ html += '<th style="width:45%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Replacement</th>';
565
+ html += '</tr></thead></table>';
566
+ html += '<div style="flex:1;overflow-y:auto;"><table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;"><tbody>';
535
567
 
536
568
  prompts.forEach(function(row) {
537
569
  const key = row.systemPrompt;
538
- const isEnabled = window._promptMockConfig[key] !== undefined;
539
- const currentVal = isEnabled ? window._promptMockConfig[key] : row.systemPrompt;
540
- const preview = key.length > 80 ? key.slice(0, 80) + '…' : key;
570
+ const existing = window._promptMockConfig[key] || { mode: 'live', replacement: '' };
571
+ const preview = key.length > 60 ? key.slice(0, 60) + '…' : key;
541
572
 
542
573
  html += '<tr class="prompt-mock-row" data-row-index="' + row.rowIndex + '" style="border-bottom:1px solid #f0f0f0;vertical-align:top;">';
543
574
 
544
- // Checkbox column
545
- html += '<td style="padding:8px 10px;white-space:nowrap;">';
546
- html += '<input type="checkbox" class="prompt-mock-enable" title="Override this system prompt"' + (isEnabled ? ' checked' : '') + ' onchange="window.onPromptMockToggle(' + row.rowIndex + ', this.checked)">';
547
- html += '</td>';
575
+ // System prompt preview column
576
+ html += '<td style="width:25%;padding:6px 10px;overflow:hidden;text-overflow:ellipsis;"><div style="font-size:11px;color:#555;font-family:Monaco,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + esc(key) + '">' + esc(preview) + '</div></td>';
548
577
 
549
- // Model column
550
- html += '<td style="padding:8px 10px;font-family:Monaco,monospace;font-size:12px;white-space:nowrap;">' + esc(row.modelName) + '</td>';
578
+ // Calls count column
579
+ html += '<td style="width:10%;padding:6px 10px;color:#555;white-space:nowrap;">' + row.count + 'x</td>';
551
580
 
552
- // Uses count column
553
- html += '<td style="padding:8px 10px;color:#555;white-space:nowrap;">' + row.count + 'x</td>';
581
+ // Mode column
582
+ html += '<td style="width:20%;padding:6px 10px;white-space:nowrap;">';
583
+ html += '<select class="prompt-mock-mode" style="font-size:12px;padding:2px 4px;width: 100%;" onchange="window.onSystemPromptMockModeChange(' + row.rowIndex + ', this.value)">';
584
+ html += '<option value="live"' + (existing.mode === 'live' ? ' selected' : '') + '>Live</option>';
585
+ html += '<option value="replace-all"' + (existing.mode === 'replace-all' ? ' selected' : '') + '>Replace All</option>';
586
+ if (row.count > 1) {
587
+ html += '<option value="replace-specific"' + (existing.mode === 'replace-specific' ? ' selected' : '') + '>Replace Specific</option>';
588
+ }
589
+ html += '</select>';
590
+ html += '</td>';
554
591
 
555
- // System prompt column
556
- html += '<td style="padding:8px 10px;width:100%;">';
557
- if (!isEnabled) {
558
- html += '<div style="font-size:11px;color:#888;font-style:italic;font-family:Monaco,monospace;max-width:380px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + esc(key) + '">' + esc(preview) + '</div>';
592
+ // Replacement column
593
+ html += '<td style="width:45%;padding:6px 10px;">';
594
+ if (existing.mode === 'replace-all') {
595
+ html += '<textarea class="prompt-mock-input" data-row-index="' + row.rowIndex + '" style="width:100%;box-sizing:border-box;font-size:11px;font-family:Monaco,monospace;padding:4px;border:1px solid #ddd;border-radius:4px;min-height:48px;resize:vertical;" oninput="window.onPromptMockInput(' + row.rowIndex + ', this.value)">' + esc(existing.replacement || key) + '</textarea>';
596
+ } else if (existing.mode === 'replace-specific') {
597
+ html += '<div style="font-size:11px;color:#555;margin-bottom:4px;">Select calls to replace:</div>';
598
+ row.calls.forEach(function(call) {
599
+ const isChecked = existing.callIndices && existing.callIndices.indexOf(call.callNumber) !== -1;
600
+ html += '<div style="margin-bottom:4px;padding:4px;background:#fafafa;border-radius:4px;border:1px solid #eee;">';
601
+ html += '<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;">';
602
+ html += '<input type="checkbox" class="prompt-call-checkbox" value="' + call.callNumber + '"' + (isChecked ? ' checked' : '') + ' onchange="window.onSystemPromptCallCheckChange(' + row.rowIndex + ',' + call.callNumber + ',this.checked)">';
603
+ html += '<span>Call #' + call.callNumber + '</span>';
604
+ html += '</label>';
605
+ html += '</div>';
606
+ });
607
+ html += '<textarea class="prompt-mock-input" data-row-index="' + row.rowIndex + '" style="width:100%;box-sizing:border-box;font-size:11px;font-family:Monaco,monospace;padding:4px;border:1px solid #ddd;border-radius:4px;min-height:48px;resize:vertical;margin-top:4px;" oninput="window.onPromptMockInput(' + row.rowIndex + ', this.value)">' + esc(existing.replacement || key) + '</textarea>';
559
608
  } else {
560
- html += '<textarea class="prompt-mock-input" data-row-index="' + row.rowIndex + '" style="width:100%;box-sizing:border-box;font-size:11px;font-family:Monaco,monospace;padding:4px;border:1px solid #ddd;border-radius:4px;min-height:72px;resize:vertical;" oninput="window.onPromptMockInput(' + row.rowIndex + ', this.value)">' + esc(currentVal) + '</textarea>';
609
+ html += '<span style="color:#aaa;font-size:11px;">—</span>';
561
610
  }
562
611
  html += '</td>';
563
612
 
564
613
  html += '</tr>';
565
614
  });
566
615
 
567
- html += '</tbody></table></div>';
616
+ html += '</tbody></table></div></div>';
568
617
  return html;
569
618
  }
570
619
 
571
620
  function buildPromptMockConfigFromUI() {
572
- // Return _promptMockConfig as-is (string key → string value), filtering out blank values
573
621
  const config = {};
574
622
  Object.keys(window._promptMockConfig).forEach(function(key) {
575
- const val = window._promptMockConfig[key];
576
- if (typeof val === 'string' && val.trim()) config[key] = val;
623
+ const entry = window._promptMockConfig[key];
624
+ if (!entry || entry.mode === 'live') return;
625
+ config[key] = { mode: entry.mode, replacement: entry.replacement || '' };
626
+ if (entry.mode === 'replace-specific' && entry.callIndices) {
627
+ config[key].callIndices = entry.callIndices;
628
+ }
577
629
  });
578
630
  return config;
579
631
  }
580
632
 
581
- window.onPromptMockToggle = function(rowIndex, enabled) {
633
+ window.onSystemPromptMockModeChange = function(rowIndex, mode) {
582
634
  const prompts = getSystemPromptsFromTrace();
583
635
  const row = prompts[rowIndex];
584
636
  if (!row) return;
585
637
  const key = row.systemPrompt;
586
- if (!enabled) {
638
+ if (mode === 'live') {
587
639
  delete window._promptMockConfig[key];
588
640
  } else {
589
- // Pre-fill with the original system prompt so user can edit from there
590
- if (window._promptMockConfig[key] === undefined) {
591
- window._promptMockConfig[key] = row.systemPrompt;
641
+ if (!window._promptMockConfig[key]) {
642
+ window._promptMockConfig[key] = { mode: mode, replacement: row.systemPrompt };
643
+ } else {
644
+ window._promptMockConfig[key].mode = mode;
645
+ }
646
+ if (mode === 'replace-specific' && !window._promptMockConfig[key].callIndices) {
647
+ window._promptMockConfig[key].callIndices = [];
592
648
  }
593
649
  }
594
650
  const container = document.getElementById('promptMockContainer');
595
651
  if (container) container.innerHTML = renderPromptMockSection();
596
652
  };
597
653
 
654
+ window.onSystemPromptCallCheckChange = function(rowIndex, callNumber, checked) {
655
+ const prompts = getSystemPromptsFromTrace();
656
+ const row = prompts[rowIndex];
657
+ if (!row) return;
658
+ const key = row.systemPrompt;
659
+ if (!window._promptMockConfig[key]) return;
660
+ var indices = window._promptMockConfig[key].callIndices || [];
661
+ if (checked) {
662
+ if (indices.indexOf(callNumber) === -1) indices.push(callNumber);
663
+ } else {
664
+ var pos = indices.indexOf(callNumber);
665
+ if (pos >= 0) indices.splice(pos, 1);
666
+ }
667
+ window._promptMockConfig[key].callIndices = indices;
668
+ };
669
+
670
+ window.switchMockTab = function(tabName) {
671
+ const tabs = {
672
+ tool: document.getElementById('mockTabTool'),
673
+ prompt: document.getElementById('mockTabPrompt'),
674
+ userPrompt: document.getElementById('mockTabUserPrompt'),
675
+ };
676
+ const contents = {
677
+ tool: document.getElementById('mockTabToolContent'),
678
+ prompt: document.getElementById('mockTabPromptContent'),
679
+ userPrompt: document.getElementById('mockTabUserPromptContent'),
680
+ };
681
+ const toolExtra = document.getElementById('mockTabToolExtra');
682
+ Object.keys(tabs).forEach(function(key) {
683
+ const isActive = key === tabName;
684
+ if (tabs[key]) { tabs[key].style.borderBottomColor = isActive ? '#4f46e5' : 'transparent'; tabs[key].style.color = isActive ? '#4f46e5' : '#888'; }
685
+ if (contents[key]) contents[key].style.display = isActive ? '' : 'none';
686
+ });
687
+ if (toolExtra) toolExtra.style.display = tabName === 'tool' ? 'flex' : 'none';
688
+ };
689
+
598
690
  window.onPromptMockInput = function(rowIndex, value) {
599
691
  const prompts = getSystemPromptsFromTrace();
600
692
  const row = prompts[rowIndex];
601
693
  if (!row) return;
602
- window._promptMockConfig[row.systemPrompt] = value;
694
+ if (window._promptMockConfig[row.systemPrompt]) {
695
+ window._promptMockConfig[row.systemPrompt].replacement = value;
696
+ }
603
697
  };
604
698
 
605
699
  window.onToolMockModeChange = function(toolName, mode) {
@@ -630,10 +724,187 @@
630
724
  if (container) container.innerHTML = renderToolMockSection(showAll && showAll.checked);
631
725
  };
632
726
 
727
+ // ---- User Prompt Mock Helpers ----
728
+
729
+ /** Extract all user-role message content strings from an LLM call input. */
730
+ function extractUserPromptsFromInput(input) {
731
+ // Unwrap JSON-encoded strings until we get a non-string value.
732
+ while (typeof input === 'string') {
733
+ try { input = JSON.parse(input); } catch(e) { break; }
734
+ }
735
+ if (!input || typeof input !== 'object') return [];
736
+ var msgs = Array.isArray(input.messages) ? input.messages : (Array.isArray(input) ? input : null);
737
+ if (!msgs) return [];
738
+ var results = [];
739
+ for (var i = 0; i < msgs.length; i++) {
740
+ var m = msgs[i];
741
+ if (m && typeof m === 'object' && m.role === 'user' && typeof m.content === 'string') {
742
+ results.push(m.content);
743
+ }
744
+ }
745
+ return results;
746
+ }
747
+
748
+ /**
749
+ * Returns an array of unique user prompt texts observed across all GENERATION observations.
750
+ * Each entry: { userPrompt, modelName, calls: [{obsIndex, callNumber}], count, rowIndex }
751
+ * callNumber is the 1-indexed occurrence of this text across GENERATION observations.
752
+ */
753
+ function getUserPromptsFromTrace() {
754
+ var seen = []; // [{ userPrompt, modelName, calls, count }]
755
+ var seenMap = {}; // userPrompt -> index in seen
756
+ var callCounters = {}; // userPrompt -> running count
757
+ currentObservations.forEach(function(obs, obsIndex) {
758
+ if (obs.type !== 'GENERATION') return;
759
+ var prompts = extractUserPromptsFromInput(obs.input);
760
+ var uniqueInCall = [];
761
+ prompts.forEach(function(p) { if (uniqueInCall.indexOf(p) === -1) uniqueInCall.push(p); });
762
+ uniqueInCall.forEach(function(p) {
763
+ callCounters[p] = (callCounters[p] || 0) + 1;
764
+ var callNumber = callCounters[p];
765
+ if (seenMap[p] === undefined) {
766
+ seenMap[p] = seen.length;
767
+ seen.push({ userPrompt: p, modelName: obs.model || obs.name || '(unknown)', calls: [], count: 0 });
768
+ }
769
+ seen[seenMap[p]].calls.push({ obsIndex: obsIndex, callNumber: callNumber });
770
+ seen[seenMap[p]].count++;
771
+ });
772
+ });
773
+ return seen.map(function(e, i) { return Object.assign({}, e, { rowIndex: i }); });
774
+ }
775
+
776
+ function renderUserPromptMockSection() {
777
+ const prompts = getUserPromptsFromTrace();
778
+ if (prompts.length === 0) {
779
+ return '<div style="color:#999;font-size:13px;padding:6px 0;">No user prompts detected in trace. Only AI calls with user-role messages can be mocked here.</div>';
780
+ }
781
+
782
+ let html = '<div style="border:1px solid #e0e0e0;border-radius:6px;height:100%;display:flex;flex-direction:column;overflow:hidden;">';
783
+ html += '<table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;">';
784
+ html += '<thead><tr style="background:#f5f5f5;">';
785
+ html += '<th style="width:30%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">User Prompt</th>';
786
+ html += '<th style="width:10%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls</th>';
787
+ html += '<th style="width:20%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mode</th>';
788
+ html += '<th style="width:40%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Replacement</th>';
789
+ html += '</tr></thead></table>';
790
+ html += '<div style="flex:1;overflow-y:auto;"><table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;"><tbody>';
791
+
792
+ prompts.forEach(function(row) {
793
+ const key = row.userPrompt;
794
+ const existing = window._userPromptMockConfig[key] || { mode: 'live', replacement: '' };
795
+ const preview = key.length > 60 ? key.slice(0, 60) + '…' : key;
796
+
797
+ html += '<tr class="user-prompt-mock-row" data-row-index="' + row.rowIndex + '" style="border-bottom:1px solid #f0f0f0;vertical-align:top;">';
798
+
799
+ // User prompt preview column
800
+ html += '<td style="width:30%;padding:6px 10px;overflow:hidden;text-overflow:ellipsis;"><div style="font-size:11px;color:#555;font-family:Monaco,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + esc(key) + '">' + esc(preview) + '</div></td>';
801
+
802
+ // Calls count column
803
+ html += '<td style="width:10%;padding:6px 10px;color:#555;white-space:nowrap;">' + row.count + 'x</td>';
804
+
805
+ // Mode column
806
+ html += '<td style="width:20%;padding:6px 10px;white-space:nowrap;">';
807
+ html += '<select class="user-prompt-mock-mode" style="font-size:12px;padding:2px 4px;width: 100%;" onchange="window.onUserPromptMockModeChange(' + row.rowIndex + ', this.value)">';
808
+ html += '<option value="live"' + (existing.mode === 'live' ? ' selected' : '') + '>Live</option>';
809
+ html += '<option value="replace-all"' + (existing.mode === 'replace-all' ? ' selected' : '') + '>Replace All</option>';
810
+ if (row.count > 1) {
811
+ html += '<option value="replace-specific"' + (existing.mode === 'replace-specific' ? ' selected' : '') + '>Replace Specific</option>';
812
+ }
813
+ html += '</select>';
814
+ html += '</td>';
815
+
816
+ // Replacement column
817
+ html += '<td style="width:40%;padding:6px 10px;">';
818
+ if (existing.mode === 'replace-all') {
819
+ html += '<textarea class="user-prompt-mock-input" data-row-index="' + row.rowIndex + '" style="width:100%;box-sizing:border-box;font-size:11px;font-family:Monaco,monospace;padding:4px;border:1px solid #ddd;border-radius:4px;min-height:48px;resize:vertical;" oninput="window.onUserPromptMockInput(' + row.rowIndex + ', this.value)">' + esc(existing.replacement || key) + '</textarea>';
820
+ } else if (existing.mode === 'replace-specific') {
821
+ html += '<div style="font-size:11px;color:#555;margin-bottom:4px;">Select calls to replace:</div>';
822
+ row.calls.forEach(function(call) {
823
+ const isChecked = existing.callIndices && existing.callIndices.indexOf(call.callNumber) !== -1;
824
+ html += '<div style="margin-bottom:4px;padding:4px;background:#fafafa;border-radius:4px;border:1px solid #eee;">';
825
+ html += '<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;">';
826
+ html += '<input type="checkbox" class="user-prompt-call-checkbox" value="' + call.callNumber + '"' + (isChecked ? ' checked' : '') + ' onchange="window.onUserPromptCallCheckChange(' + row.rowIndex + ',' + call.callNumber + ',this.checked)">';
827
+ html += '<span>Call #' + call.callNumber + '</span>';
828
+ html += '</label>';
829
+ html += '</div>';
830
+ });
831
+ html += '<textarea class="user-prompt-mock-input" data-row-index="' + row.rowIndex + '" style="width:100%;box-sizing:border-box;font-size:11px;font-family:Monaco,monospace;padding:4px;border:1px solid #ddd;border-radius:4px;min-height:48px;resize:vertical;margin-top:4px;" oninput="window.onUserPromptMockInput(' + row.rowIndex + ', this.value)">' + esc(existing.replacement || key) + '</textarea>';
832
+ } else {
833
+ html += '<span style="color:#aaa;font-size:11px;">—</span>';
834
+ }
835
+ html += '</td>';
836
+
837
+ html += '</tr>';
838
+ });
839
+
840
+ html += '</tbody></table></div></div>';
841
+ return html;
842
+ }
843
+
844
+ function buildUserPromptMockConfigFromUI() {
845
+ const config = {};
846
+ Object.keys(window._userPromptMockConfig).forEach(function(key) {
847
+ const entry = window._userPromptMockConfig[key];
848
+ if (!entry || entry.mode === 'live') return;
849
+ config[key] = { mode: entry.mode, replacement: entry.replacement || '' };
850
+ if (entry.mode === 'replace-specific' && entry.callIndices) {
851
+ config[key].callIndices = entry.callIndices;
852
+ }
853
+ });
854
+ return config;
855
+ }
856
+
857
+ window.onUserPromptMockModeChange = function(rowIndex, mode) {
858
+ const prompts = getUserPromptsFromTrace();
859
+ const row = prompts[rowIndex];
860
+ if (!row) return;
861
+ const key = row.userPrompt;
862
+ if (mode === 'live') {
863
+ delete window._userPromptMockConfig[key];
864
+ } else {
865
+ if (!window._userPromptMockConfig[key]) {
866
+ window._userPromptMockConfig[key] = { mode: mode, replacement: row.userPrompt };
867
+ } else {
868
+ window._userPromptMockConfig[key].mode = mode;
869
+ }
870
+ if (mode === 'replace-specific' && !window._userPromptMockConfig[key].callIndices) {
871
+ window._userPromptMockConfig[key].callIndices = [];
872
+ }
873
+ }
874
+ const container = document.getElementById('userPromptMockContainer');
875
+ if (container) container.innerHTML = renderUserPromptMockSection();
876
+ };
877
+
878
+ window.onUserPromptMockInput = function(rowIndex, value) {
879
+ const prompts = getUserPromptsFromTrace();
880
+ const row = prompts[rowIndex];
881
+ if (!row) return;
882
+ if (window._userPromptMockConfig[row.userPrompt]) {
883
+ window._userPromptMockConfig[row.userPrompt].replacement = value;
884
+ }
885
+ };
886
+
887
+ window.onUserPromptCallCheckChange = function(rowIndex, callNumber, checked) {
888
+ const prompts = getUserPromptsFromTrace();
889
+ const row = prompts[rowIndex];
890
+ if (!row) return;
891
+ const key = row.userPrompt;
892
+ if (!window._userPromptMockConfig[key]) return;
893
+ var indices = window._userPromptMockConfig[key].callIndices || [];
894
+ if (checked) {
895
+ if (indices.indexOf(callNumber) === -1) indices.push(callNumber);
896
+ } else {
897
+ var pos = indices.indexOf(callNumber);
898
+ if (pos >= 0) indices.splice(pos, 1);
899
+ }
900
+ window._userPromptMockConfig[key].callIndices = indices;
901
+ };
902
+
633
903
  window.openLiveValidationDialog = function() {
634
904
  if (window.liveValidationDialog) return;
635
905
  window._toolMockConfig = {}; // Reset mock configs each time dialog opens
636
906
  window._promptMockConfig = {};
907
+ window._userPromptMockConfig = {};
637
908
 
638
909
  const hasTraceTools = currentObservations.some(function(o) { return o.type === 'TOOL'; });
639
910
  const hasRegisteredTools = codeIndex.tools && codeIndex.tools.length > 0;
@@ -642,7 +913,7 @@
642
913
  window.liveValidationDialog.id = 'liveValidationDialog';
643
914
  window.liveValidationDialog.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.25);display:flex;align-items:center;justify-content:center;z-index:9999;';
644
915
  window.liveValidationDialog.innerHTML = `
645
- <div style="background:white;padding:32px 28px;border-radius:12px;box-shadow:0 2px 24px #0002;min-width:680px;max-width:90vw;max-height:90vh;overflow-y:auto;">
916
+ <div style="background:white;padding:32px 28px;border-radius:12px;box-shadow:0 2px 24px #0002;width:680px;max-width:90vw;max-height:90vh;overflow-y:auto;">
646
917
  <h3 style="margin-top:0;margin-bottom:18px;font-size:20px;">Validate Updated Flow with Live Data</h3>
647
918
  <label style="font-size:15px;display:block;margin-bottom:8px;">How many times do you want to run the flow with live data?</label>
648
919
  <input id="liveValidationCount" type="number" min="1" value="1" style="width:100%;font-size:16px;padding:6px 10px;margin-bottom:18px;" />
@@ -650,23 +921,29 @@
650
921
  <input id="liveValidationSequential" type="checkbox" />
651
922
  Run in sequence instead of parallel
652
923
  </label>
653
- ${(hasTraceTools || hasRegisteredTools) ? `
654
924
  <div style="border-top:1px solid #eee;padding-top:16px;margin-bottom:16px;">
655
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
656
- <div style="font-size:15px;font-weight:600;">Tool Mocking</div>
657
- <label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
658
- <input id="showAllToolsToggle" type="checkbox" onchange="document.getElementById('toolMockContainer').innerHTML = renderToolMockSection(this.checked);" />
659
- Show all registered tools
660
- </label>
925
+ <div style="display:flex;align-items:center;gap:0;border-bottom:2px solid #e0e0e0;margin-bottom:0;">
926
+ ${(hasTraceTools || hasRegisteredTools) ? `<button id="mockTabTool" class="mock-tab-btn" onclick="window.switchMockTab('tool')" style="padding:8px 18px;font-size:14px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid #4f46e5;color:#4f46e5;margin-bottom:-2px;">Tool Mocking <span class="help-icon-wrap"><span class="help-icon">?</span><span class="help-tooltip">Control which tools use real calls vs mock data. &lsquo;Live&rsquo; runs the real tool, &lsquo;Mock All Calls&rsquo; returns mock data for every call, and &lsquo;Mock Specific Calls&rsquo; lets you pick which call indices to mock.</span></span></button>` : ''}
927
+ <button id="mockTabPrompt" class="mock-tab-btn" onclick="window.switchMockTab('prompt')" style="padding:8px 18px;font-size:14px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid ${(hasTraceTools || hasRegisteredTools) ? 'transparent' : '#4f46e5'};color:${(hasTraceTools || hasRegisteredTools) ? '#888' : '#4f46e5'};margin-bottom:-2px;">System Prompt <span class="help-icon-wrap"><span class="help-icon">?</span><span class="help-tooltip">Override the system prompt for LLM calls. Check a row to enable editing &mdash; the replacement applies to all LLM calls that use that exact system prompt text.</span></span></button>
928
+ <button id="mockTabUserPrompt" class="mock-tab-btn" onclick="window.switchMockTab('userPrompt')" style="padding:8px 18px;font-size:14px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#888;margin-bottom:-2px;">User Prompt <span class="help-icon-wrap"><span class="help-icon">?</span><span class="help-tooltip">Override user-role messages in LLM calls. Each unique user message text is listed. Choose &lsquo;Replace All&rsquo; to replace it in every LLM call where it appears, or &lsquo;Replace Specific&rsquo; to only replace it in selected call occurrences.</span></span></button>
929
+ <div id="mockTabToolExtra" style="margin-left:auto;display:flex;align-items:center;">
930
+ ${(hasTraceTools || hasRegisteredTools) ? `<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
931
+ <input id="showAllToolsToggle" type="checkbox" onchange="document.getElementById('toolMockContainer').innerHTML = renderToolMockSection(this.checked);" />
932
+ Show all tools
933
+ </label>` : ''}
934
+ </div>
661
935
  </div>
662
- <div id="toolMockContainer" style="max-height:160px;overflow-y:auto;"></div>
663
- </div>` : ''}
664
- <div style="border-top:1px solid #eee;padding-top:16px;margin-bottom:16px;">
665
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
666
- <div style="font-size:15px;font-weight:600;">Prompt Mocking</div>
667
- <span style="font-size:12px;color:#888;">Check a row to replace the system prompt for all calls using it</span>
936
+ <div style="height:300px;position:relative;">
937
+ <div id="mockTabToolContent" style="height:100%;overflow:hidden;${(hasTraceTools || hasRegisteredTools) ? '' : 'display:none;'}">
938
+ <div id="toolMockContainer" style="height:100%;"></div>
939
+ </div>
940
+ <div id="mockTabPromptContent" style="height:100%;overflow:hidden;${(hasTraceTools || hasRegisteredTools) ? 'display:none;' : ''}">
941
+ <div id="promptMockContainer" style="height:100%;"></div>
942
+ </div>
943
+ <div id="mockTabUserPromptContent" style="height:100%;overflow:hidden;display:none;">
944
+ <div id="userPromptMockContainer" style="height:100%;"></div>
945
+ </div>
668
946
  </div>
669
- <div id="promptMockContainer" style="max-height:160px;overflow-y:auto;"></div>
670
947
  </div>
671
948
  <div style="display:flex;gap:12px;justify-content:space-between;align-items:center;">
672
949
  <span id="liveValidationProgress" style="font-size:14px;color:#555;"></span>
@@ -687,6 +964,10 @@
687
964
  if (promptMockContainer) {
688
965
  promptMockContainer.innerHTML = renderPromptMockSection();
689
966
  }
967
+ const userPromptMockContainer = document.getElementById('userPromptMockContainer');
968
+ if (userPromptMockContainer) {
969
+ userPromptMockContainer.innerHTML = renderUserPromptMockSection();
970
+ }
690
971
  document.getElementById('cancelLiveValidation').onclick = function() {
691
972
  window.liveValidationDialog.remove();
692
973
  window.liveValidationDialog = null;
@@ -700,6 +981,8 @@
700
981
  window._toolMockConfig = toolMockConfig;
701
982
  const promptMockConfig = buildPromptMockConfigFromUI();
702
983
  window._promptMockConfig = promptMockConfig;
984
+ const userPromptMockConfig = buildUserPromptMockConfigFromUI();
985
+ window._userPromptMockConfig = userPromptMockConfig;
703
986
  const submitBtn = document.getElementById('submitLiveValidation');
704
987
  submitBtn.disabled = true;
705
988
  submitBtn.textContent = 'Validating...';
@@ -736,7 +1019,7 @@
736
1019
  const collectedTraces = [];
737
1020
  let fatalError = null;
738
1021
  for (let i = 0; i < count; i++) {
739
- const singlePayload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig };
1022
+ const singlePayload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig, userPromptMockConfig };
740
1023
  try {
741
1024
  const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(singlePayload) });
742
1025
  const data = await response.json();
@@ -755,7 +1038,7 @@
755
1038
  } else {
756
1039
  // Parallel mode: single bulk request
757
1040
  if (progressEl) progressEl.textContent = `Running ${count} workflow run${count !== 1 ? 's' : ''} in parallel…`;
758
- const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig };
1041
+ const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig, userPromptMockConfig };
759
1042
  try {
760
1043
  const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
761
1044
  const data = await response.json();
@@ -947,6 +1230,7 @@
947
1230
  }
948
1231
  }
949
1232
 
1233
+
950
1234
  function renderObservationTable() {
951
1235
  // Capture scroll positions for step 5 columns before rebuilding DOM
952
1236
  let _step5Col1Scroll = 0, _step5Col2Scroll = 0;
@@ -1460,6 +1744,7 @@
1460
1744
  observations: currentObservations,
1461
1745
  toolMockConfig: window._toolMockConfig || {},
1462
1746
  promptMockConfig: window._promptMockConfig || {},
1747
+ userPromptMockConfig: window._userPromptMockConfig || {},
1463
1748
  };
1464
1749
  const response = await fetch('/api/run-from-breakpoint', {
1465
1750
  method: 'POST',
@@ -1491,7 +1776,7 @@
1491
1776
  step5RerunInFlight = true;
1492
1777
  renderObservationTable();
1493
1778
  try {
1494
- const payload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig: window._toolMockConfig || {}, promptMockConfig: window._promptMockConfig || {} };
1779
+ const payload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig: window._toolMockConfig || {}, promptMockConfig: window._promptMockConfig || {}, userPromptMockConfig: window._userPromptMockConfig || {} };
1495
1780
  const response = await fetch('/api/validate-workflow', {
1496
1781
  method: 'POST',
1497
1782
  headers: { 'Content-Type': 'application/json' },
@@ -1543,6 +1828,7 @@
1543
1828
  snapshotId: liveTrace.snapshotId,
1544
1829
  toolMockConfig: window._toolMockConfig || {},
1545
1830
  promptMockConfig: window._promptMockConfig || {},
1831
+ userPromptMockConfig: window._userPromptMockConfig || {},
1546
1832
  };
1547
1833
  const response = await fetch('/api/resume-agent-from-task', {
1548
1834
  method: 'POST',