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
@@ -156,6 +156,28 @@ function buildToolArgs(input, tool) {
156
156
  }
157
157
  return [input];
158
158
  }
159
+ /**
160
+ * Flatten structured prompt mock config to simple Record<string, string> for HTTP mode.
161
+ * Accepts both the new structured format { mode, replacement } and legacy string values.
162
+ * Only entries with mode !== 'live' (or plain string entries) are included.
163
+ */
164
+ function flattenPromptMockConfig(raw) {
165
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
166
+ return {};
167
+ const result = {};
168
+ for (const [key, val] of Object.entries(raw)) {
169
+ if (typeof val === 'string') {
170
+ result[key] = val;
171
+ }
172
+ else if (val && typeof val === 'object' && 'replacement' in val) {
173
+ const entry = val;
174
+ if (entry.mode !== 'live' && typeof entry.replacement === 'string') {
175
+ result[key] = entry.replacement;
176
+ }
177
+ }
178
+ }
179
+ return result;
180
+ }
159
181
  function formatError(error) {
160
182
  if (error instanceof Error)
161
183
  return error.message;
@@ -313,6 +335,7 @@ function runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowN
313
335
  ...(options?.toolMockConfig !== undefined ? { toolMockConfig: options.toolMockConfig } : {}),
314
336
  ...(options?.aiMockConfig !== undefined ? { aiMockConfig: options.aiMockConfig } : {}),
315
337
  ...(options?.promptMockConfig !== undefined ? { promptMockConfig: options.promptMockConfig } : {}),
338
+ ...(options?.userPromptMockConfig !== undefined ? { userPromptMockConfig: options.userPromptMockConfig } : {}),
316
339
  });
317
340
  child.stdin.write(payload);
318
341
  child.stdin.end(); // Always close stdin to avoid subprocess hang
@@ -363,8 +386,9 @@ async function runGenerationObservation(observation) {
363
386
  async function rerunObservation(cwd, observation, tools) {
364
387
  const type = observation.type?.toUpperCase();
365
388
  const name = observation.name ?? '(unknown)';
366
- if (type === 'TOOL' || name.startsWith('tool-')) {
367
- observation.name = name.startsWith('tool-') ? name.slice(5) : name; // Support both explicit type and name prefix for tool observations
389
+ const isToolByName = name.startsWith('tool-') || name.startsWith('tool:');
390
+ if (type === 'TOOL' || isToolByName) {
391
+ observation.name = isToolByName ? name.slice(5) : name; // Support both explicit type and name prefix for tool observations
368
392
  return runToolObservation(cwd, observation, tools);
369
393
  }
370
394
  if (type === 'GENERATION') {
@@ -643,6 +667,10 @@ async function validateWorkflowRuns(cwd, body) {
643
667
  const promptMockConfig = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
644
668
  ? body.promptMockConfig
645
669
  : undefined;
670
+ // Parse user prompt mock config if provided
671
+ const userPromptMockConfig = body.userPromptMockConfig && typeof body.userPromptMockConfig === 'object' && !Array.isArray(body.userPromptMockConfig)
672
+ ? body.userPromptMockConfig
673
+ : undefined;
646
674
  const workflowsModulePath = resolveWorkflowModule(cwd);
647
675
  if (!workflowsModulePath) {
648
676
  return {
@@ -658,7 +686,7 @@ async function validateWorkflowRuns(cwd, body) {
658
686
  console.log(`[elasticdash] Running workflow "${workflowName}" ${runCount} time(s) in ${mode} mode via subprocess`);
659
687
  async function runOne(runNumber) {
660
688
  console.log(`[elasticdash] === Run ${runNumber}: Starting workflow "${workflowName}" ===`);
661
- const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, (toolMockConfig || aiMockConfig || promptMockConfig) ? { ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}) } : undefined)
689
+ const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, (toolMockConfig || aiMockConfig || promptMockConfig || userPromptMockConfig) ? { ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}), ...(userPromptMockConfig ? { userPromptMockConfig } : {}) } : undefined)
662
690
  .catch(err => {
663
691
  throw { ok: false, error: `Workflow subprocess failed: ${formatError(err)}` };
664
692
  });
@@ -969,6 +997,30 @@ function getDashboardHtml() {
969
997
  <meta name="viewport" content="width=device-width, initial-scale=1">
970
998
  <title>ElasticDash Dashboard</title>
971
999
  <style>
1000
+ /* Help icon tooltip */
1001
+ .help-icon-wrap { position: relative; display: inline-flex; align-items: center; }
1002
+ .help-icon {
1003
+ display: inline-flex; align-items: center; justify-content: center;
1004
+ width: 16px; height: 16px; border-radius: 50%;
1005
+ background: #e0e0e0; color: #666; font-size: 11px; font-weight: bold;
1006
+ cursor: help; vertical-align: middle; margin-left: 4px;
1007
+ user-select: none;
1008
+ }
1009
+ .help-tooltip {
1010
+ display: none; position: absolute; left: 50%; bottom: calc(100% + 8px);
1011
+ transform: translateX(-50%);
1012
+ background: #333; color: #fff; font-size: 12px; font-weight: 400;
1013
+ line-height: 1.4; padding: 8px 12px; border-radius: 6px;
1014
+ width: 280px; white-space: normal; z-index: 10000;
1015
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
1016
+ pointer-events: auto;
1017
+ }
1018
+ .help-tooltip::after {
1019
+ content: ''; position: absolute; top: 100%; left: 50%;
1020
+ transform: translateX(-50%);
1021
+ border: 6px solid transparent; border-top-color: #333;
1022
+ }
1023
+ .help-icon-wrap:hover .help-tooltip { display: block; }
972
1024
  /* Ensure first cell in observation-table never overflows parent */
973
1025
  .observation-table td:first-child {
974
1026
  max-width: 120px;
@@ -1278,7 +1330,7 @@ function getDashboardHtml() {
1278
1330
  window._toolMockConfig = {}; // { toolName: { mode: 'live'|'mock-all'|'mock-specific', callIndices: [], mockData: {} } }
1279
1331
 
1280
1332
  // ---- Prompt Mock Config State ----
1281
- // { [originalSystemPrompt]: newSystemPrompt } only keys where user enabled the override
1333
+ // { [originalSystemPrompt]: { mode, replacement, callIndices? } }
1282
1334
  window._promptMockConfig = {};
1283
1335
 
1284
1336
  function getToolsFromTrace() {
@@ -1286,7 +1338,7 @@ function getDashboardHtml() {
1286
1338
  const toolCalls = {};
1287
1339
  currentObservations.forEach(function(obs, i) {
1288
1340
  const isToolByType = obs.type === 'TOOL';
1289
- const isToolByName = typeof obs.name === 'string' && obs.name.startsWith('tool-');
1341
+ const isToolByName = typeof obs.name === 'string' && (obs.name.startsWith('tool-') || obs.name.startsWith('tool:'));
1290
1342
  if (!isToolByType && !isToolByName) return;
1291
1343
  const name = isToolByName && obs.type !== 'TOOL'
1292
1344
  ? obs.name.slice(5)
@@ -1372,14 +1424,15 @@ function getDashboardHtml() {
1372
1424
  return '<div style="color:#999;font-size:13px;padding:6px 0;">No tools detected.</div>';
1373
1425
  }
1374
1426
 
1375
- let html = '<div style="max-height:260px;overflow-y:auto;border:1px solid #e0e0e0;border-radius:6px;">';
1376
- html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
1427
+ let html = '<div style="border:1px solid #e0e0e0;border-radius:6px;height:100%;display:flex;flex-direction:column;overflow:hidden;">';
1428
+ html += '<table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;">';
1377
1429
  html += '<thead><tr style="background:#f5f5f5;">';
1378
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Tool</th>';
1379
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls in Trace</th>';
1380
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mock Mode</th>';
1381
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Details</th>';
1382
- html += '</tr></thead><tbody>';
1430
+ html += '<th style="width:30%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Tool</th>';
1431
+ html += '<th style="width:15%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls</th>';
1432
+ html += '<th style="width:20%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mode</th>';
1433
+ html += '<th style="width:35%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Details</th>';
1434
+ html += '</tr></thead></table>';
1435
+ html += '<div style="flex:1;overflow-y:auto;"><table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;"><tbody>';
1383
1436
 
1384
1437
  toolNames.forEach(function(name) {
1385
1438
  const calls = traceTools[name] || [];
@@ -1388,10 +1441,10 @@ function getDashboardHtml() {
1388
1441
  const nameStyle = inTrace ? '' : 'color:#999;';
1389
1442
 
1390
1443
  html += '<tr class="tool-mock-row" data-tool-name="' + esc(name) + '" style="border-bottom:1px solid #f0f0f0;">';
1391
- 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>';
1392
- html += '<td style="padding:6px 10px;">' + calls.length + '</td>';
1393
- html += '<td style="padding:6px 10px;">';
1394
- html += '<select class="tool-mock-mode" style="font-size:12px;padding:2px 4px;" onchange="window.onToolMockModeChange(\\'' + esc(name) + '\\', this.value)">';
1444
+ 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>';
1445
+ html += '<td style="width:15%;padding:6px 10px;">' + calls.length + '</td>';
1446
+ html += '<td style="width:20%;padding:6px 10px;">';
1447
+ html += '<select class="tool-mock-mode" style="font-size:12px;padding:2px 4px;width: 100%;" onchange="window.onToolMockModeChange(\\'' + esc(name) + '\\', this.value)">';
1395
1448
  html += '<option value="live"' + (existing.mode === 'live' ? ' selected' : '') + '>Live</option>';
1396
1449
  html += '<option value="mock-all"' + (existing.mode === 'mock-all' ? ' selected' : '') + '>Mock All Calls</option>';
1397
1450
  if (calls.length > 0) {
@@ -1401,7 +1454,7 @@ function getDashboardHtml() {
1401
1454
  html += '</td>';
1402
1455
 
1403
1456
  // Details column: per-call checkboxes + mock data inputs
1404
- html += '<td style="padding:6px 10px;">';
1457
+ html += '<td style="width:35%;padding:6px 10px;">';
1405
1458
  if (existing.mode === 'mock-all') {
1406
1459
  let defaultData = (existing.mockData && existing.mockData[0] !== undefined) ? JSON.stringify(existing.mockData[0]) : (calls.length > 0 ? JSON.stringify(calls[0].output) : '');
1407
1460
  defaultData = convert(defaultData);
@@ -1432,7 +1485,7 @@ function getDashboardHtml() {
1432
1485
  html += '</tr>';
1433
1486
  });
1434
1487
 
1435
- html += '</tbody></table></div>';
1488
+ html += '</tbody></table></div></div>';
1436
1489
  return html;
1437
1490
  }
1438
1491
 
@@ -1440,9 +1493,10 @@ function getDashboardHtml() {
1440
1493
 
1441
1494
  /** Extract the system prompt string from an LLM call input object or JSON string. */
1442
1495
  function extractSystemPromptFromInput(input) {
1443
- // Input may arrive as a JSON-encoded string (e.g. from Langfuse traces)
1444
- if (typeof input === 'string') {
1445
- try { input = JSON.parse(input); } catch(e) { return null; }
1496
+ // Input may arrive as a JSON-encoded string (e.g. from Langfuse traces).
1497
+ // Unwrap until we get a non-string value.
1498
+ while (typeof input === 'string') {
1499
+ try { input = JSON.parse(input); } catch(e) { break; }
1446
1500
  }
1447
1501
  if (!input || typeof input !== 'object') return null;
1448
1502
  // Anthropic style: { system: "...", messages: [...] }
@@ -1464,19 +1518,24 @@ function getDashboardHtml() {
1464
1518
 
1465
1519
  /**
1466
1520
  * Returns an array of unique system prompts observed across all GENERATION observations.
1467
- * Each entry: { systemPrompt, modelName, count, rowIndex }
1521
+ * Each entry: { systemPrompt, modelName, calls: [{obsIndex, callNumber}], count, rowIndex }
1522
+ * callNumber is the 1-indexed occurrence of this prompt across GENERATION observations.
1468
1523
  */
1469
1524
  function getSystemPromptsFromTrace() {
1470
- var seen = []; // [{ systemPrompt, modelName, count }]
1525
+ var seen = []; // [{ systemPrompt, modelName, calls, count }]
1471
1526
  var seenMap = {}; // systemPrompt -> index in seen
1472
- currentObservations.forEach(function(obs) {
1527
+ var callCounters = {}; // systemPrompt -> running count
1528
+ currentObservations.forEach(function(obs, obsIndex) {
1473
1529
  if (obs.type !== 'GENERATION') return;
1474
1530
  var sp = extractSystemPromptFromInput(obs.input);
1475
1531
  if (!sp) return;
1532
+ callCounters[sp] = (callCounters[sp] || 0) + 1;
1533
+ var callNumber = callCounters[sp];
1476
1534
  if (seenMap[sp] === undefined) {
1477
1535
  seenMap[sp] = seen.length;
1478
- seen.push({ systemPrompt: sp, modelName: obs.model || obs.name || '(unknown)', count: 0 });
1536
+ seen.push({ systemPrompt: sp, modelName: obs.model || obs.name || '(unknown)', calls: [], count: 0 });
1479
1537
  }
1538
+ seen[seenMap[sp]].calls.push({ obsIndex: obsIndex, callNumber: callNumber });
1480
1539
  seen[seenMap[sp]].count++;
1481
1540
  });
1482
1541
  return seen.map(function(e, i) { return Object.assign({}, e, { rowIndex: i }); });
@@ -1488,82 +1547,145 @@ function getDashboardHtml() {
1488
1547
  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>';
1489
1548
  }
1490
1549
 
1491
- let html = '<div style="max-height:360px;overflow-y:auto;border:1px solid #e0e0e0;border-radius:6px;">';
1492
- html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
1550
+ let html = '<div style="border:1px solid #e0e0e0;border-radius:6px;height:100%;display:flex;flex-direction:column;overflow:hidden;">';
1551
+ html += '<table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;">';
1493
1552
  html += '<thead><tr style="background:#f5f5f5;">';
1494
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;width:24px;"></th>';
1495
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Model</th>';
1496
- html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Uses</th>';
1497
- 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>';
1498
- html += '</tr></thead><tbody>';
1553
+ html += '<th style="width:25%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">System Prompt</th>';
1554
+ html += '<th style="width:10%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls</th>';
1555
+ html += '<th style="width:20%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mode</th>';
1556
+ html += '<th style="width:45%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Replacement</th>';
1557
+ html += '</tr></thead></table>';
1558
+ html += '<div style="flex:1;overflow-y:auto;"><table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;"><tbody>';
1499
1559
 
1500
1560
  prompts.forEach(function(row) {
1501
1561
  const key = row.systemPrompt;
1502
- const isEnabled = window._promptMockConfig[key] !== undefined;
1503
- const currentVal = isEnabled ? window._promptMockConfig[key] : row.systemPrompt;
1504
- const preview = key.length > 80 ? key.slice(0, 80) + '…' : key;
1562
+ const existing = window._promptMockConfig[key] || { mode: 'live', replacement: '' };
1563
+ const preview = key.length > 60 ? key.slice(0, 60) + '…' : key;
1505
1564
 
1506
1565
  html += '<tr class="prompt-mock-row" data-row-index="' + row.rowIndex + '" style="border-bottom:1px solid #f0f0f0;vertical-align:top;">';
1507
1566
 
1508
- // Checkbox column
1509
- html += '<td style="padding:8px 10px;white-space:nowrap;">';
1510
- html += '<input type="checkbox" class="prompt-mock-enable" title="Override this system prompt"' + (isEnabled ? ' checked' : '') + ' onchange="window.onPromptMockToggle(' + row.rowIndex + ', this.checked)">';
1511
- html += '</td>';
1567
+ // System prompt preview column
1568
+ 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>';
1512
1569
 
1513
- // Model column
1514
- html += '<td style="padding:8px 10px;font-family:Monaco,monospace;font-size:12px;white-space:nowrap;">' + esc(row.modelName) + '</td>';
1570
+ // Calls count column
1571
+ html += '<td style="width:10%;padding:6px 10px;color:#555;white-space:nowrap;">' + row.count + 'x</td>';
1515
1572
 
1516
- // Uses count column
1517
- html += '<td style="padding:8px 10px;color:#555;white-space:nowrap;">' + row.count + 'x</td>';
1573
+ // Mode column
1574
+ html += '<td style="width:20%;padding:6px 10px;white-space:nowrap;">';
1575
+ html += '<select class="prompt-mock-mode" style="font-size:12px;padding:2px 4px;width: 100%;" onchange="window.onSystemPromptMockModeChange(' + row.rowIndex + ', this.value)">';
1576
+ html += '<option value="live"' + (existing.mode === 'live' ? ' selected' : '') + '>Live</option>';
1577
+ html += '<option value="replace-all"' + (existing.mode === 'replace-all' ? ' selected' : '') + '>Replace All</option>';
1578
+ if (row.count > 1) {
1579
+ html += '<option value="replace-specific"' + (existing.mode === 'replace-specific' ? ' selected' : '') + '>Replace Specific</option>';
1580
+ }
1581
+ html += '</select>';
1582
+ html += '</td>';
1518
1583
 
1519
- // System prompt column
1520
- html += '<td style="padding:8px 10px;width:100%;">';
1521
- if (!isEnabled) {
1522
- 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>';
1584
+ // Replacement column
1585
+ html += '<td style="width:45%;padding:6px 10px;">';
1586
+ if (existing.mode === 'replace-all') {
1587
+ 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>';
1588
+ } else if (existing.mode === 'replace-specific') {
1589
+ html += '<div style="font-size:11px;color:#555;margin-bottom:4px;">Select calls to replace:</div>';
1590
+ row.calls.forEach(function(call) {
1591
+ const isChecked = existing.callIndices && existing.callIndices.indexOf(call.callNumber) !== -1;
1592
+ html += '<div style="margin-bottom:4px;padding:4px;background:#fafafa;border-radius:4px;border:1px solid #eee;">';
1593
+ html += '<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;">';
1594
+ html += '<input type="checkbox" class="prompt-call-checkbox" value="' + call.callNumber + '"' + (isChecked ? ' checked' : '') + ' onchange="window.onSystemPromptCallCheckChange(' + row.rowIndex + ',' + call.callNumber + ',this.checked)">';
1595
+ html += '<span>Call #' + call.callNumber + '</span>';
1596
+ html += '</label>';
1597
+ html += '</div>';
1598
+ });
1599
+ 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>';
1523
1600
  } else {
1524
- 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>';
1601
+ html += '<span style="color:#aaa;font-size:11px;">—</span>';
1525
1602
  }
1526
1603
  html += '</td>';
1527
1604
 
1528
1605
  html += '</tr>';
1529
1606
  });
1530
1607
 
1531
- html += '</tbody></table></div>';
1608
+ html += '</tbody></table></div></div>';
1532
1609
  return html;
1533
1610
  }
1534
1611
 
1535
1612
  function buildPromptMockConfigFromUI() {
1536
- // Return _promptMockConfig as-is (string key → string value), filtering out blank values
1537
1613
  const config = {};
1538
1614
  Object.keys(window._promptMockConfig).forEach(function(key) {
1539
- const val = window._promptMockConfig[key];
1540
- if (typeof val === 'string' && val.trim()) config[key] = val;
1615
+ const entry = window._promptMockConfig[key];
1616
+ if (!entry || entry.mode === 'live') return;
1617
+ config[key] = { mode: entry.mode, replacement: entry.replacement || '' };
1618
+ if (entry.mode === 'replace-specific' && entry.callIndices) {
1619
+ config[key].callIndices = entry.callIndices;
1620
+ }
1541
1621
  });
1542
1622
  return config;
1543
1623
  }
1544
1624
 
1545
- window.onPromptMockToggle = function(rowIndex, enabled) {
1625
+ window.onSystemPromptMockModeChange = function(rowIndex, mode) {
1546
1626
  const prompts = getSystemPromptsFromTrace();
1547
1627
  const row = prompts[rowIndex];
1548
1628
  if (!row) return;
1549
1629
  const key = row.systemPrompt;
1550
- if (!enabled) {
1630
+ if (mode === 'live') {
1551
1631
  delete window._promptMockConfig[key];
1552
1632
  } else {
1553
- // Pre-fill with the original system prompt so user can edit from there
1554
- if (window._promptMockConfig[key] === undefined) {
1555
- window._promptMockConfig[key] = row.systemPrompt;
1633
+ if (!window._promptMockConfig[key]) {
1634
+ window._promptMockConfig[key] = { mode: mode, replacement: row.systemPrompt };
1635
+ } else {
1636
+ window._promptMockConfig[key].mode = mode;
1637
+ }
1638
+ if (mode === 'replace-specific' && !window._promptMockConfig[key].callIndices) {
1639
+ window._promptMockConfig[key].callIndices = [];
1556
1640
  }
1557
1641
  }
1558
1642
  const container = document.getElementById('promptMockContainer');
1559
1643
  if (container) container.innerHTML = renderPromptMockSection();
1560
1644
  };
1561
1645
 
1646
+ window.onSystemPromptCallCheckChange = function(rowIndex, callNumber, checked) {
1647
+ const prompts = getSystemPromptsFromTrace();
1648
+ const row = prompts[rowIndex];
1649
+ if (!row) return;
1650
+ const key = row.systemPrompt;
1651
+ if (!window._promptMockConfig[key]) return;
1652
+ var indices = window._promptMockConfig[key].callIndices || [];
1653
+ if (checked) {
1654
+ if (indices.indexOf(callNumber) === -1) indices.push(callNumber);
1655
+ } else {
1656
+ var pos = indices.indexOf(callNumber);
1657
+ if (pos >= 0) indices.splice(pos, 1);
1658
+ }
1659
+ window._promptMockConfig[key].callIndices = indices;
1660
+ };
1661
+
1662
+ window.switchMockTab = function(tabName) {
1663
+ const tabs = {
1664
+ tool: document.getElementById('mockTabTool'),
1665
+ prompt: document.getElementById('mockTabPrompt'),
1666
+ userPrompt: document.getElementById('mockTabUserPrompt'),
1667
+ };
1668
+ const contents = {
1669
+ tool: document.getElementById('mockTabToolContent'),
1670
+ prompt: document.getElementById('mockTabPromptContent'),
1671
+ userPrompt: document.getElementById('mockTabUserPromptContent'),
1672
+ };
1673
+ const toolExtra = document.getElementById('mockTabToolExtra');
1674
+ Object.keys(tabs).forEach(function(key) {
1675
+ const isActive = key === tabName;
1676
+ if (tabs[key]) { tabs[key].style.borderBottomColor = isActive ? '#4f46e5' : 'transparent'; tabs[key].style.color = isActive ? '#4f46e5' : '#888'; }
1677
+ if (contents[key]) contents[key].style.display = isActive ? '' : 'none';
1678
+ });
1679
+ if (toolExtra) toolExtra.style.display = tabName === 'tool' ? 'flex' : 'none';
1680
+ };
1681
+
1562
1682
  window.onPromptMockInput = function(rowIndex, value) {
1563
1683
  const prompts = getSystemPromptsFromTrace();
1564
1684
  const row = prompts[rowIndex];
1565
1685
  if (!row) return;
1566
- window._promptMockConfig[row.systemPrompt] = value;
1686
+ if (window._promptMockConfig[row.systemPrompt]) {
1687
+ window._promptMockConfig[row.systemPrompt].replacement = value;
1688
+ }
1567
1689
  };
1568
1690
 
1569
1691
  window.onToolMockModeChange = function(toolName, mode) {
@@ -1594,10 +1716,187 @@ function getDashboardHtml() {
1594
1716
  if (container) container.innerHTML = renderToolMockSection(showAll && showAll.checked);
1595
1717
  };
1596
1718
 
1719
+ // ---- User Prompt Mock Helpers ----
1720
+
1721
+ /** Extract all user-role message content strings from an LLM call input. */
1722
+ function extractUserPromptsFromInput(input) {
1723
+ // Unwrap JSON-encoded strings until we get a non-string value.
1724
+ while (typeof input === 'string') {
1725
+ try { input = JSON.parse(input); } catch(e) { break; }
1726
+ }
1727
+ if (!input || typeof input !== 'object') return [];
1728
+ var msgs = Array.isArray(input.messages) ? input.messages : (Array.isArray(input) ? input : null);
1729
+ if (!msgs) return [];
1730
+ var results = [];
1731
+ for (var i = 0; i < msgs.length; i++) {
1732
+ var m = msgs[i];
1733
+ if (m && typeof m === 'object' && m.role === 'user' && typeof m.content === 'string') {
1734
+ results.push(m.content);
1735
+ }
1736
+ }
1737
+ return results;
1738
+ }
1739
+
1740
+ /**
1741
+ * Returns an array of unique user prompt texts observed across all GENERATION observations.
1742
+ * Each entry: { userPrompt, modelName, calls: [{obsIndex, callNumber}], count, rowIndex }
1743
+ * callNumber is the 1-indexed occurrence of this text across GENERATION observations.
1744
+ */
1745
+ function getUserPromptsFromTrace() {
1746
+ var seen = []; // [{ userPrompt, modelName, calls, count }]
1747
+ var seenMap = {}; // userPrompt -> index in seen
1748
+ var callCounters = {}; // userPrompt -> running count
1749
+ currentObservations.forEach(function(obs, obsIndex) {
1750
+ if (obs.type !== 'GENERATION') return;
1751
+ var prompts = extractUserPromptsFromInput(obs.input);
1752
+ var uniqueInCall = [];
1753
+ prompts.forEach(function(p) { if (uniqueInCall.indexOf(p) === -1) uniqueInCall.push(p); });
1754
+ uniqueInCall.forEach(function(p) {
1755
+ callCounters[p] = (callCounters[p] || 0) + 1;
1756
+ var callNumber = callCounters[p];
1757
+ if (seenMap[p] === undefined) {
1758
+ seenMap[p] = seen.length;
1759
+ seen.push({ userPrompt: p, modelName: obs.model || obs.name || '(unknown)', calls: [], count: 0 });
1760
+ }
1761
+ seen[seenMap[p]].calls.push({ obsIndex: obsIndex, callNumber: callNumber });
1762
+ seen[seenMap[p]].count++;
1763
+ });
1764
+ });
1765
+ return seen.map(function(e, i) { return Object.assign({}, e, { rowIndex: i }); });
1766
+ }
1767
+
1768
+ function renderUserPromptMockSection() {
1769
+ const prompts = getUserPromptsFromTrace();
1770
+ if (prompts.length === 0) {
1771
+ 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>';
1772
+ }
1773
+
1774
+ let html = '<div style="border:1px solid #e0e0e0;border-radius:6px;height:100%;display:flex;flex-direction:column;overflow:hidden;">';
1775
+ html += '<table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;">';
1776
+ html += '<thead><tr style="background:#f5f5f5;">';
1777
+ html += '<th style="width:30%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">User Prompt</th>';
1778
+ html += '<th style="width:10%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls</th>';
1779
+ html += '<th style="width:20%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mode</th>';
1780
+ html += '<th style="width:40%;padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Replacement</th>';
1781
+ html += '</tr></thead></table>';
1782
+ html += '<div style="flex:1;overflow-y:auto;"><table style="width:100%;border-collapse:collapse;font-size:13px;table-layout:fixed;"><tbody>';
1783
+
1784
+ prompts.forEach(function(row) {
1785
+ const key = row.userPrompt;
1786
+ const existing = window._userPromptMockConfig[key] || { mode: 'live', replacement: '' };
1787
+ const preview = key.length > 60 ? key.slice(0, 60) + '…' : key;
1788
+
1789
+ html += '<tr class="user-prompt-mock-row" data-row-index="' + row.rowIndex + '" style="border-bottom:1px solid #f0f0f0;vertical-align:top;">';
1790
+
1791
+ // User prompt preview column
1792
+ 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>';
1793
+
1794
+ // Calls count column
1795
+ html += '<td style="width:10%;padding:6px 10px;color:#555;white-space:nowrap;">' + row.count + 'x</td>';
1796
+
1797
+ // Mode column
1798
+ html += '<td style="width:20%;padding:6px 10px;white-space:nowrap;">';
1799
+ html += '<select class="user-prompt-mock-mode" style="font-size:12px;padding:2px 4px;width: 100%;" onchange="window.onUserPromptMockModeChange(' + row.rowIndex + ', this.value)">';
1800
+ html += '<option value="live"' + (existing.mode === 'live' ? ' selected' : '') + '>Live</option>';
1801
+ html += '<option value="replace-all"' + (existing.mode === 'replace-all' ? ' selected' : '') + '>Replace All</option>';
1802
+ if (row.count > 1) {
1803
+ html += '<option value="replace-specific"' + (existing.mode === 'replace-specific' ? ' selected' : '') + '>Replace Specific</option>';
1804
+ }
1805
+ html += '</select>';
1806
+ html += '</td>';
1807
+
1808
+ // Replacement column
1809
+ html += '<td style="width:40%;padding:6px 10px;">';
1810
+ if (existing.mode === 'replace-all') {
1811
+ 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>';
1812
+ } else if (existing.mode === 'replace-specific') {
1813
+ html += '<div style="font-size:11px;color:#555;margin-bottom:4px;">Select calls to replace:</div>';
1814
+ row.calls.forEach(function(call) {
1815
+ const isChecked = existing.callIndices && existing.callIndices.indexOf(call.callNumber) !== -1;
1816
+ html += '<div style="margin-bottom:4px;padding:4px;background:#fafafa;border-radius:4px;border:1px solid #eee;">';
1817
+ html += '<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;">';
1818
+ html += '<input type="checkbox" class="user-prompt-call-checkbox" value="' + call.callNumber + '"' + (isChecked ? ' checked' : '') + ' onchange="window.onUserPromptCallCheckChange(' + row.rowIndex + ',' + call.callNumber + ',this.checked)">';
1819
+ html += '<span>Call #' + call.callNumber + '</span>';
1820
+ html += '</label>';
1821
+ html += '</div>';
1822
+ });
1823
+ 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>';
1824
+ } else {
1825
+ html += '<span style="color:#aaa;font-size:11px;">—</span>';
1826
+ }
1827
+ html += '</td>';
1828
+
1829
+ html += '</tr>';
1830
+ });
1831
+
1832
+ html += '</tbody></table></div></div>';
1833
+ return html;
1834
+ }
1835
+
1836
+ function buildUserPromptMockConfigFromUI() {
1837
+ const config = {};
1838
+ Object.keys(window._userPromptMockConfig).forEach(function(key) {
1839
+ const entry = window._userPromptMockConfig[key];
1840
+ if (!entry || entry.mode === 'live') return;
1841
+ config[key] = { mode: entry.mode, replacement: entry.replacement || '' };
1842
+ if (entry.mode === 'replace-specific' && entry.callIndices) {
1843
+ config[key].callIndices = entry.callIndices;
1844
+ }
1845
+ });
1846
+ return config;
1847
+ }
1848
+
1849
+ window.onUserPromptMockModeChange = function(rowIndex, mode) {
1850
+ const prompts = getUserPromptsFromTrace();
1851
+ const row = prompts[rowIndex];
1852
+ if (!row) return;
1853
+ const key = row.userPrompt;
1854
+ if (mode === 'live') {
1855
+ delete window._userPromptMockConfig[key];
1856
+ } else {
1857
+ if (!window._userPromptMockConfig[key]) {
1858
+ window._userPromptMockConfig[key] = { mode: mode, replacement: row.userPrompt };
1859
+ } else {
1860
+ window._userPromptMockConfig[key].mode = mode;
1861
+ }
1862
+ if (mode === 'replace-specific' && !window._userPromptMockConfig[key].callIndices) {
1863
+ window._userPromptMockConfig[key].callIndices = [];
1864
+ }
1865
+ }
1866
+ const container = document.getElementById('userPromptMockContainer');
1867
+ if (container) container.innerHTML = renderUserPromptMockSection();
1868
+ };
1869
+
1870
+ window.onUserPromptMockInput = function(rowIndex, value) {
1871
+ const prompts = getUserPromptsFromTrace();
1872
+ const row = prompts[rowIndex];
1873
+ if (!row) return;
1874
+ if (window._userPromptMockConfig[row.userPrompt]) {
1875
+ window._userPromptMockConfig[row.userPrompt].replacement = value;
1876
+ }
1877
+ };
1878
+
1879
+ window.onUserPromptCallCheckChange = function(rowIndex, callNumber, checked) {
1880
+ const prompts = getUserPromptsFromTrace();
1881
+ const row = prompts[rowIndex];
1882
+ if (!row) return;
1883
+ const key = row.userPrompt;
1884
+ if (!window._userPromptMockConfig[key]) return;
1885
+ var indices = window._userPromptMockConfig[key].callIndices || [];
1886
+ if (checked) {
1887
+ if (indices.indexOf(callNumber) === -1) indices.push(callNumber);
1888
+ } else {
1889
+ var pos = indices.indexOf(callNumber);
1890
+ if (pos >= 0) indices.splice(pos, 1);
1891
+ }
1892
+ window._userPromptMockConfig[key].callIndices = indices;
1893
+ };
1894
+
1597
1895
  window.openLiveValidationDialog = function() {
1598
1896
  if (window.liveValidationDialog) return;
1599
1897
  window._toolMockConfig = {}; // Reset mock configs each time dialog opens
1600
1898
  window._promptMockConfig = {};
1899
+ window._userPromptMockConfig = {};
1601
1900
 
1602
1901
  const hasTraceTools = currentObservations.some(function(o) { return o.type === 'TOOL'; });
1603
1902
  const hasRegisteredTools = codeIndex.tools && codeIndex.tools.length > 0;
@@ -1606,7 +1905,7 @@ function getDashboardHtml() {
1606
1905
  window.liveValidationDialog.id = 'liveValidationDialog';
1607
1906
  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;';
1608
1907
  window.liveValidationDialog.innerHTML = \`
1609
- <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;">
1908
+ <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;">
1610
1909
  <h3 style="margin-top:0;margin-bottom:18px;font-size:20px;">Validate Updated Flow with Live Data</h3>
1611
1910
  <label style="font-size:15px;display:block;margin-bottom:8px;">How many times do you want to run the flow with live data?</label>
1612
1911
  <input id="liveValidationCount" type="number" min="1" value="1" style="width:100%;font-size:16px;padding:6px 10px;margin-bottom:18px;" />
@@ -1614,23 +1913,29 @@ function getDashboardHtml() {
1614
1913
  <input id="liveValidationSequential" type="checkbox" />
1615
1914
  Run in sequence instead of parallel
1616
1915
  </label>
1617
- \${(hasTraceTools || hasRegisteredTools) ? \`
1618
1916
  <div style="border-top:1px solid #eee;padding-top:16px;margin-bottom:16px;">
1619
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
1620
- <div style="font-size:15px;font-weight:600;">Tool Mocking</div>
1621
- <label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
1622
- <input id="showAllToolsToggle" type="checkbox" onchange="document.getElementById('toolMockContainer').innerHTML = renderToolMockSection(this.checked);" />
1623
- Show all registered tools
1624
- </label>
1917
+ <div style="display:flex;align-items:center;gap:0;border-bottom:2px solid #e0e0e0;margin-bottom:0;">
1918
+ \${(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>\` : ''}
1919
+ <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>
1920
+ <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>
1921
+ <div id="mockTabToolExtra" style="margin-left:auto;display:flex;align-items:center;">
1922
+ \${(hasTraceTools || hasRegisteredTools) ? \`<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
1923
+ <input id="showAllToolsToggle" type="checkbox" onchange="document.getElementById('toolMockContainer').innerHTML = renderToolMockSection(this.checked);" />
1924
+ Show all tools
1925
+ </label>\` : ''}
1926
+ </div>
1625
1927
  </div>
1626
- <div id="toolMockContainer" style="max-height:160px;overflow-y:auto;"></div>
1627
- </div>\` : ''}
1628
- <div style="border-top:1px solid #eee;padding-top:16px;margin-bottom:16px;">
1629
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
1630
- <div style="font-size:15px;font-weight:600;">Prompt Mocking</div>
1631
- <span style="font-size:12px;color:#888;">Check a row to replace the system prompt for all calls using it</span>
1928
+ <div style="height:300px;position:relative;">
1929
+ <div id="mockTabToolContent" style="height:100%;overflow:hidden;\${(hasTraceTools || hasRegisteredTools) ? '' : 'display:none;'}">
1930
+ <div id="toolMockContainer" style="height:100%;"></div>
1931
+ </div>
1932
+ <div id="mockTabPromptContent" style="height:100%;overflow:hidden;\${(hasTraceTools || hasRegisteredTools) ? 'display:none;' : ''}">
1933
+ <div id="promptMockContainer" style="height:100%;"></div>
1934
+ </div>
1935
+ <div id="mockTabUserPromptContent" style="height:100%;overflow:hidden;display:none;">
1936
+ <div id="userPromptMockContainer" style="height:100%;"></div>
1937
+ </div>
1632
1938
  </div>
1633
- <div id="promptMockContainer" style="max-height:160px;overflow-y:auto;"></div>
1634
1939
  </div>
1635
1940
  <div style="display:flex;gap:12px;justify-content:space-between;align-items:center;">
1636
1941
  <span id="liveValidationProgress" style="font-size:14px;color:#555;"></span>
@@ -1651,6 +1956,10 @@ function getDashboardHtml() {
1651
1956
  if (promptMockContainer) {
1652
1957
  promptMockContainer.innerHTML = renderPromptMockSection();
1653
1958
  }
1959
+ const userPromptMockContainer = document.getElementById('userPromptMockContainer');
1960
+ if (userPromptMockContainer) {
1961
+ userPromptMockContainer.innerHTML = renderUserPromptMockSection();
1962
+ }
1654
1963
  document.getElementById('cancelLiveValidation').onclick = function() {
1655
1964
  window.liveValidationDialog.remove();
1656
1965
  window.liveValidationDialog = null;
@@ -1664,6 +1973,8 @@ function getDashboardHtml() {
1664
1973
  window._toolMockConfig = toolMockConfig;
1665
1974
  const promptMockConfig = buildPromptMockConfigFromUI();
1666
1975
  window._promptMockConfig = promptMockConfig;
1976
+ const userPromptMockConfig = buildUserPromptMockConfigFromUI();
1977
+ window._userPromptMockConfig = userPromptMockConfig;
1667
1978
  const submitBtn = document.getElementById('submitLiveValidation');
1668
1979
  submitBtn.disabled = true;
1669
1980
  submitBtn.textContent = 'Validating...';
@@ -1700,7 +2011,7 @@ function getDashboardHtml() {
1700
2011
  const collectedTraces = [];
1701
2012
  let fatalError = null;
1702
2013
  for (let i = 0; i < count; i++) {
1703
- const singlePayload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig };
2014
+ const singlePayload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig, userPromptMockConfig };
1704
2015
  try {
1705
2016
  const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(singlePayload) });
1706
2017
  const data = await response.json();
@@ -1719,7 +2030,7 @@ function getDashboardHtml() {
1719
2030
  } else {
1720
2031
  // Parallel mode: single bulk request
1721
2032
  if (progressEl) progressEl.textContent = \`Running \${count} workflow run\${count !== 1 ? 's' : ''} in parallel…\`;
1722
- const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig };
2033
+ const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig, userPromptMockConfig };
1723
2034
  try {
1724
2035
  const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
1725
2036
  const data = await response.json();
@@ -1911,6 +2222,7 @@ function getDashboardHtml() {
1911
2222
  }
1912
2223
  }
1913
2224
 
2225
+
1914
2226
  function renderObservationTable() {
1915
2227
  // Capture scroll positions for step 5 columns before rebuilding DOM
1916
2228
  let _step5Col1Scroll = 0, _step5Col2Scroll = 0;
@@ -2424,6 +2736,7 @@ function getDashboardHtml() {
2424
2736
  observations: currentObservations,
2425
2737
  toolMockConfig: window._toolMockConfig || {},
2426
2738
  promptMockConfig: window._promptMockConfig || {},
2739
+ userPromptMockConfig: window._userPromptMockConfig || {},
2427
2740
  };
2428
2741
  const response = await fetch('/api/run-from-breakpoint', {
2429
2742
  method: 'POST',
@@ -2455,7 +2768,7 @@ function getDashboardHtml() {
2455
2768
  step5RerunInFlight = true;
2456
2769
  renderObservationTable();
2457
2770
  try {
2458
- const payload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig: window._toolMockConfig || {}, promptMockConfig: window._promptMockConfig || {} };
2771
+ const payload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig: window._toolMockConfig || {}, promptMockConfig: window._promptMockConfig || {}, userPromptMockConfig: window._userPromptMockConfig || {} };
2459
2772
  const response = await fetch('/api/validate-workflow', {
2460
2773
  method: 'POST',
2461
2774
  headers: { 'Content-Type': 'application/json' },
@@ -2507,6 +2820,7 @@ function getDashboardHtml() {
2507
2820
  snapshotId: liveTrace.snapshotId,
2508
2821
  toolMockConfig: window._toolMockConfig || {},
2509
2822
  promptMockConfig: window._promptMockConfig || {},
2823
+ userPromptMockConfig: window._userPromptMockConfig || {},
2510
2824
  };
2511
2825
  const response = await fetch('/api/resume-agent-from-task', {
2512
2826
  method: 'POST',
@@ -3027,11 +3341,11 @@ function resolveTemplateValue(value, input) {
3027
3341
  return value;
3028
3342
  }
3029
3343
  async function runHttpWorkflow(opts) {
3030
- const { workflowName, workflowInput, frozenEvents = [], promptMocks = {}, pushedEvents, runConfigs, config, dashboardPort } = opts;
3344
+ const { workflowName, workflowInput, frozenEvents = [], promptMocks = {}, userPromptMocks, toolMockConfig, aiMockConfig, pushedEvents, runConfigs, config, dashboardPort } = opts;
3031
3345
  const runId = randomUUID();
3032
- // Register run config so the user's server can fetch frozen events and prompt mocks
3346
+ // Register run config so the user's server can fetch frozen events, prompt mocks, and output mocks
3033
3347
  pushedEvents.set(runId, []);
3034
- runConfigs.set(runId, { frozenEvents, promptMocks });
3348
+ runConfigs.set(runId, { frozenEvents, promptMocks, userPromptMocks, toolMockConfig, aiMockConfig });
3035
3349
  try {
3036
3350
  const parsedInput = parseObservationInput(workflowInput);
3037
3351
  const inputObj = parsedInput && typeof parsedInput === 'object' && !Array.isArray(parsedInput) ? parsedInput : {};
@@ -3176,14 +3490,21 @@ export async function startDashboardServer(cwd, options = {}) {
3176
3490
  const resolvedInput = resolveWorkflowArgsFromObservations(body, workflowName);
3177
3491
  const workflowInput = resolvedInput.input ?? null;
3178
3492
  const traces = [];
3179
- const promptMocks = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
3180
- ? body.promptMockConfig
3181
- : {};
3493
+ const promptMocks = flattenPromptMockConfig(body.promptMockConfig);
3494
+ const valUserPromptMocks = body.userPromptMockConfig && typeof body.userPromptMockConfig === 'object' && !Array.isArray(body.userPromptMockConfig)
3495
+ ? body.userPromptMockConfig
3496
+ : undefined;
3497
+ const valToolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
3498
+ ? body.toolMockConfig
3499
+ : undefined;
3500
+ const valAiMockConfig = body.aiMockConfig && typeof body.aiMockConfig === 'object' && !Array.isArray(body.aiMockConfig)
3501
+ ? body.aiMockConfig
3502
+ : undefined;
3182
3503
  const runOne = async (runNumber) => {
3183
3504
  const result = await runHttpWorkflow({
3184
3505
  workflowName, workflowInput, pushedEvents, runConfigs,
3185
3506
  config: httpConfig, dashboardPort: port,
3186
- promptMocks,
3507
+ promptMocks, userPromptMocks: valUserPromptMocks, toolMockConfig: valToolMockConfig, aiMockConfig: valAiMockConfig,
3187
3508
  });
3188
3509
  const traceStub = { getSteps: () => [], getLLMSteps: () => [], getToolCalls: () => [], getCustomSteps: () => [], recordLLMStep: () => { }, recordToolCall: () => { }, recordCustomStep: () => { } };
3189
3510
  return {
@@ -3251,14 +3572,22 @@ export async function startDashboardServer(cwd, options = {}) {
3251
3572
  const httpConfig = elasticdashConfig.workflows?.[workflowName];
3252
3573
  if (httpConfig?.mode === 'http') {
3253
3574
  // HTTP workflow mode — call user's dev server with frozen events + prompt mocks for step replay
3254
- const bpPromptMocks = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
3255
- ? body.promptMockConfig
3256
- : {};
3575
+ const bpPromptMocks = flattenPromptMockConfig(body.promptMockConfig);
3576
+ const bpUserPromptMocks = body.userPromptMockConfig && typeof body.userPromptMockConfig === 'object' && !Array.isArray(body.userPromptMockConfig)
3577
+ ? body.userPromptMockConfig
3578
+ : undefined;
3579
+ const bpToolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
3580
+ ? body.toolMockConfig
3581
+ : undefined;
3582
+ const bpAiMockConfig = body.aiMockConfig && typeof body.aiMockConfig === 'object' && !Array.isArray(body.aiMockConfig)
3583
+ ? body.aiMockConfig
3584
+ : undefined;
3257
3585
  console.log(`[elasticdash] Run from breakpoint (HTTP mode): workflow="${workflowName}" checkpoint=${checkpoint} frozen=${frozenEvents.length}`);
3258
3586
  const result = await runHttpWorkflow({
3259
3587
  workflowName, workflowInput, pushedEvents, runConfigs,
3260
3588
  config: httpConfig, dashboardPort: port,
3261
- frozenEvents, promptMocks: bpPromptMocks,
3589
+ frozenEvents, promptMocks: bpPromptMocks, userPromptMocks: bpUserPromptMocks,
3590
+ toolMockConfig: bpToolMockConfig, aiMockConfig: bpAiMockConfig,
3262
3591
  });
3263
3592
  const traceStub = { getSteps: () => [], getLLMSteps: () => [], getToolCalls: () => [], getCustomSteps: () => [], recordLLMStep: () => { }, recordToolCall: () => { }, recordCustomStep: () => { } };
3264
3593
  const snapshotId = result.workflowTrace ? saveSnapshot(cwd, result.workflowTrace) : undefined;
@@ -3292,8 +3621,11 @@ export async function startDashboardServer(cwd, options = {}) {
3292
3621
  const promptMockConfig = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
3293
3622
  ? body.promptMockConfig
3294
3623
  : undefined;
3624
+ const userPromptMockConfig = body.userPromptMockConfig && typeof body.userPromptMockConfig === 'object' && !Array.isArray(body.userPromptMockConfig)
3625
+ ? body.userPromptMockConfig
3626
+ : undefined;
3295
3627
  console.log(`[elasticdash] Run from breakpoint: workflow="${workflowName}" checkpoint=${checkpoint} historyLen=${history.length}`);
3296
- const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, { replayMode: true, checkpoint, history, ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}) });
3628
+ const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, { replayMode: true, checkpoint, history, ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}), ...(userPromptMockConfig ? { userPromptMockConfig } : {}) });
3297
3629
  const traceStub = {
3298
3630
  getSteps: () => (result.steps ?? []),
3299
3631
  getLLMSteps: () => (result.llmSteps ?? []),
@@ -3370,8 +3702,11 @@ export async function startDashboardServer(cwd, options = {}) {
3370
3702
  const promptMockConfig = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
3371
3703
  ? body.promptMockConfig
3372
3704
  : undefined;
3705
+ const userPromptMockConfig = body.userPromptMockConfig && typeof body.userPromptMockConfig === 'object' && !Array.isArray(body.userPromptMockConfig)
3706
+ ? body.userPromptMockConfig
3707
+ : undefined;
3373
3708
  console.log(`[elasticdash] Resume agent from task: workflow="${workflowName}" taskIndex=${taskIndex}`);
3374
- const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, [], null, { replayMode: history.length > 0, checkpoint: 0, history, agentState, ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}) });
3709
+ const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, [], null, { replayMode: history.length > 0, checkpoint: 0, history, agentState, ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}), ...(userPromptMockConfig ? { userPromptMockConfig } : {}) });
3375
3710
  const traceStub = {
3376
3711
  getSteps: () => (result.steps ?? []),
3377
3712
  getLLMSteps: () => (result.llmSteps ?? []),
@@ -3422,7 +3757,7 @@ export async function startDashboardServer(cwd, options = {}) {
3422
3757
  const runId = url.pathname.slice('/api/run-configs/'.length);
3423
3758
  const cfg = runConfigs.get(runId);
3424
3759
  res.writeHead(cfg ? 200 : 404, { 'Content-Type': 'application/json' });
3425
- res.end(JSON.stringify({ frozenEvents: cfg?.frozenEvents ?? [], promptMocks: cfg?.promptMocks ?? {} }));
3760
+ res.end(JSON.stringify({ frozenEvents: cfg?.frozenEvents ?? [], promptMocks: cfg?.promptMocks ?? {}, ...(cfg?.userPromptMocks ? { userPromptMocks: cfg.userPromptMocks } : {}), ...(cfg?.toolMockConfig ? { toolMockConfig: cfg.toolMockConfig } : {}), ...(cfg?.aiMockConfig ? { aiMockConfig: cfg.aiMockConfig } : {}) }));
3426
3761
  }
3427
3762
  else if (url.pathname === '/api/trace-events' && req.method === 'POST') {
3428
3763
  // Receive telemetry events pushed from wrapAI / wrapTool in HTTP workflow mode