norn-cli 1.3.17 → 1.3.19

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.
@@ -0,0 +1,4674 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ResponsePanel = void 0;
37
+ const vscode = __importStar(require("vscode"));
38
+ const path = __importStar(require("path"));
39
+ const compareContentProvider_1 = require("./compareContentProvider");
40
+ const schemaGenerator_1 = require("./schemaGenerator");
41
+ class ResponsePanel {
42
+ static currentPanel;
43
+ _panel;
44
+ _disposables = [];
45
+ _sequenceName = '';
46
+ _totalSteps = 0;
47
+ _startTime = 0;
48
+ _baselineResponse = null;
49
+ _sourceFilePath = '';
50
+ constructor(panel) {
51
+ this._panel = panel;
52
+ this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
53
+ // Handle messages from the webview for response comparison
54
+ this._panel.webview.onDidReceiveMessage(async (message) => {
55
+ switch (message.type) {
56
+ case 'setBaseline':
57
+ this._baselineResponse = message.body;
58
+ break;
59
+ case 'compare':
60
+ if (this._baselineResponse) {
61
+ await this._openDiff(this._baselineResponse, message.body);
62
+ this._baselineResponse = null;
63
+ // Tell webview to reset compare state
64
+ this._panel.webview.postMessage({ type: 'resetCompare' });
65
+ }
66
+ break;
67
+ case 'assertSchema':
68
+ await this._generateSchemaAndInsertAssertion(message.body, message.method, message.url, message.variableName);
69
+ break;
70
+ case 'assertProperty':
71
+ await this._insertPropertyAssertion(message.path, message.value, message.variableName);
72
+ break;
73
+ case 'openContractView':
74
+ // Open the Contract View with the schema validation data
75
+ vscode.commands.executeCommand('norn.openContractView', {
76
+ schemaPath: message.schemaPath,
77
+ errors: message.errors,
78
+ responseBody: message.responseBody,
79
+ schema: message.schema,
80
+ sourceFile: this._sourceFilePath
81
+ });
82
+ break;
83
+ }
84
+ }, null, this._disposables);
85
+ }
86
+ /**
87
+ * Generate a JSON Schema from response body and insert assertion into the source file
88
+ */
89
+ async _generateSchemaAndInsertAssertion(body, method, url, variableName) {
90
+ if (!this._sourceFilePath) {
91
+ vscode.window.showErrorMessage('Cannot generate schema: source file path not available');
92
+ return;
93
+ }
94
+ try {
95
+ // Parse body if it's a string
96
+ const bodyObj = typeof body === 'string' ? JSON.parse(body) : body;
97
+ // Generate schema file name from endpoint
98
+ const schemaFileName = (0, schemaGenerator_1.generateSchemaFileName)(method, url);
99
+ // Create schemas directory path
100
+ const sourceDir = path.dirname(this._sourceFilePath);
101
+ const schemasDir = path.join(sourceDir, 'schemas');
102
+ const schemaFilePath = path.join(schemasDir, schemaFileName);
103
+ const relativeSchemaPath = `./schemas/${schemaFileName}`;
104
+ // Generate schema from body
105
+ const schema = (0, schemaGenerator_1.generateJsonSchema)(bodyObj, `${method} ${url} response`);
106
+ // Save schema file
107
+ (0, schemaGenerator_1.saveSchema)(schema, schemaFilePath);
108
+ // Build the assertion line
109
+ const assertLine = `assert ${variableName}.body matchesSchema "${relativeSchemaPath}"`;
110
+ // Insert assertion at the end of the sequence in the source file
111
+ await this._insertAssertionInSequence(assertLine);
112
+ // Show success message
113
+ vscode.window.showInformationMessage(`Schema generated: ${relativeSchemaPath}`);
114
+ // Open the schema file for review
115
+ const schemaDoc = await vscode.workspace.openTextDocument(schemaFilePath);
116
+ await vscode.window.showTextDocument(schemaDoc, { viewColumn: vscode.ViewColumn.Beside, preview: true });
117
+ }
118
+ catch (e) {
119
+ const error = e instanceof Error ? e.message : String(e);
120
+ vscode.window.showErrorMessage(`Failed to generate schema: ${error}`);
121
+ }
122
+ }
123
+ /**
124
+ * Insert an assertion line at the end of the current sequence in the source file
125
+ */
126
+ async _insertAssertionInSequence(assertLine) {
127
+ if (!this._sourceFilePath || !this._sequenceName) {
128
+ return;
129
+ }
130
+ const doc = await vscode.workspace.openTextDocument(this._sourceFilePath);
131
+ const text = doc.getText();
132
+ const lines = text.split('\n');
133
+ // Find the sequence by name
134
+ let sequenceStartLine = -1;
135
+ let sequenceEndLine = -1;
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const line = lines[i].trim();
138
+ // Match sequence header (with optional tags)
139
+ if (line.match(new RegExp(`^(test\\s+)?sequence\\s+${this._escapeRegex(this._sequenceName)}(\\s|$)`))) {
140
+ sequenceStartLine = i;
141
+ // Find the end of the sequence (next sequence definition or end of file)
142
+ for (let j = i + 1; j < lines.length; j++) {
143
+ const nextLine = lines[j].trim();
144
+ // Only another sequence definition ends the current sequence
145
+ if (nextLine.match(/^(test\s+)?sequence\s+\S/)) {
146
+ // Found next sequence - current sequence ends before this
147
+ sequenceEndLine = j - 1;
148
+ break;
149
+ }
150
+ }
151
+ if (sequenceEndLine === -1) {
152
+ sequenceEndLine = lines.length - 1;
153
+ }
154
+ break;
155
+ }
156
+ }
157
+ if (sequenceStartLine === -1) {
158
+ vscode.window.showWarningMessage(`Could not find sequence "${this._sequenceName}" to insert assertion`);
159
+ return;
160
+ }
161
+ // Look for explicit "end sequence" marker within the sequence bounds
162
+ let endSequenceLine = -1;
163
+ for (let j = sequenceStartLine + 1; j <= sequenceEndLine; j++) {
164
+ if (lines[j].trim().match(/^end\s+sequence\s*$/i)) {
165
+ endSequenceLine = j;
166
+ break;
167
+ }
168
+ }
169
+ let insertLine;
170
+ if (endSequenceLine !== -1) {
171
+ // Insert before "end sequence"
172
+ insertLine = endSequenceLine;
173
+ }
174
+ else {
175
+ // Find the last non-empty line within the sequence
176
+ insertLine = sequenceEndLine;
177
+ while (insertLine > sequenceStartLine && lines[insertLine].trim() === '') {
178
+ insertLine--;
179
+ }
180
+ // Insert after the last non-empty line
181
+ insertLine = insertLine + 1;
182
+ }
183
+ const insertPosition = new vscode.Position(insertLine, 0);
184
+ const edit = new vscode.WorkspaceEdit();
185
+ edit.insert(doc.uri, insertPosition, ` ${assertLine}\n`);
186
+ await vscode.workspace.applyEdit(edit);
187
+ // Save the document
188
+ await doc.save();
189
+ }
190
+ /**
191
+ * Escape special regex characters in a string
192
+ */
193
+ _escapeRegex(str) {
194
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
195
+ }
196
+ /**
197
+ * Insert an assertion for a specific property value clicked in the response panel
198
+ */
199
+ async _insertPropertyAssertion(path, value, variableName) {
200
+ // Format the value appropriately for the assertion
201
+ let valueString;
202
+ if (typeof value === 'string') {
203
+ valueString = `"${value.replace(/"/g, '\\"')}"`;
204
+ }
205
+ else if (value === null) {
206
+ valueString = 'null';
207
+ }
208
+ else {
209
+ valueString = String(value);
210
+ }
211
+ const assertLine = `assert ${variableName}.${path} == ${valueString}`;
212
+ await this._insertAssertionInSequence(assertLine);
213
+ vscode.window.showInformationMessage(`Assertion added: ${path}`);
214
+ }
215
+ /**
216
+ * Open VS Code diff view comparing two response bodies
217
+ */
218
+ async _openDiff(baseline, current) {
219
+ const provider = (0, compareContentProvider_1.getCompareContentProvider)();
220
+ // Pretty-print JSON if possible
221
+ const formatJson = (str) => {
222
+ try {
223
+ const parsed = JSON.parse(str);
224
+ return JSON.stringify(parsed, null, 2);
225
+ }
226
+ catch {
227
+ return str;
228
+ }
229
+ };
230
+ const baselineUri = provider.setContent('baseline.json', formatJson(baseline));
231
+ const currentUri = provider.setContent('current.json', formatJson(current));
232
+ await vscode.commands.executeCommand('vscode.diff', baselineUri, currentUri, 'Response Comparison (Baseline ↔ Current)');
233
+ }
234
+ static show(extensionUri, method, url, response) {
235
+ const column = vscode.ViewColumn.Beside;
236
+ // If we already have a panel, show it
237
+ if (ResponsePanel.currentPanel) {
238
+ ResponsePanel.currentPanel._panel.reveal(column);
239
+ ResponsePanel.currentPanel._update(method, url, response);
240
+ return;
241
+ }
242
+ // Otherwise, create a new panel
243
+ const panel = vscode.window.createWebviewPanel('nornResponse', 'Response', column, {
244
+ enableScripts: true,
245
+ retainContextWhenHidden: true,
246
+ });
247
+ ResponsePanel.currentPanel = new ResponsePanel(panel);
248
+ ResponsePanel.currentPanel._update(method, url, response);
249
+ }
250
+ static showError(extensionUri, method, url, errorMessage) {
251
+ const column = vscode.ViewColumn.Beside;
252
+ // If we already have a panel, show it
253
+ if (ResponsePanel.currentPanel) {
254
+ ResponsePanel.currentPanel._panel.reveal(column);
255
+ ResponsePanel.currentPanel._updateError(method, url, errorMessage);
256
+ return;
257
+ }
258
+ // Otherwise, create a new panel
259
+ const panel = vscode.window.createWebviewPanel('nornResponse', 'Error', column, {
260
+ enableScripts: true,
261
+ retainContextWhenHidden: true,
262
+ });
263
+ ResponsePanel.currentPanel = new ResponsePanel(panel);
264
+ ResponsePanel.currentPanel._updateError(method, url, errorMessage);
265
+ }
266
+ static showSequenceResult(extensionUri, result) {
267
+ const column = vscode.ViewColumn.Beside;
268
+ // If we already have a panel, show it
269
+ if (ResponsePanel.currentPanel) {
270
+ ResponsePanel.currentPanel._panel.reveal(column);
271
+ ResponsePanel.currentPanel._updateSequence(result);
272
+ return;
273
+ }
274
+ // Otherwise, create a new panel
275
+ const panel = vscode.window.createWebviewPanel('nornResponse', 'Sequence Result', column, {
276
+ enableScripts: true,
277
+ retainContextWhenHidden: true,
278
+ });
279
+ ResponsePanel.currentPanel = new ResponsePanel(panel);
280
+ ResponsePanel.currentPanel._updateSequence(result);
281
+ }
282
+ /**
283
+ * Show aggregated results for multiple runs of the same sequence (e.g. @data/@theory cases).
284
+ */
285
+ static showMultiSequenceResult(extensionUri, sequenceName, runs) {
286
+ const column = vscode.ViewColumn.Beside;
287
+ const normalizedRuns = runs.map((run, index) => ({
288
+ label: run.label || `Case ${index + 1}`,
289
+ result: run.result
290
+ }));
291
+ // If we already have a panel, show it
292
+ if (ResponsePanel.currentPanel) {
293
+ ResponsePanel.currentPanel._panel.reveal(column);
294
+ ResponsePanel.currentPanel._updateMultiSequence(sequenceName, normalizedRuns);
295
+ return;
296
+ }
297
+ // Otherwise, create a new panel
298
+ const panel = vscode.window.createWebviewPanel('nornResponse', 'Sequence Result', column, {
299
+ enableScripts: true,
300
+ retainContextWhenHidden: true,
301
+ });
302
+ ResponsePanel.currentPanel = new ResponsePanel(panel);
303
+ ResponsePanel.currentPanel._updateMultiSequence(sequenceName, normalizedRuns);
304
+ }
305
+ /**
306
+ * Start a sequence run - shows the panel with running state and prepares for incremental updates
307
+ */
308
+ static startSequenceRun(extensionUri, sequenceName, totalSteps, sourceFilePath) {
309
+ const column = vscode.ViewColumn.Beside;
310
+ // If we already have a panel, reuse it
311
+ if (ResponsePanel.currentPanel) {
312
+ ResponsePanel.currentPanel._panel.reveal(column);
313
+ }
314
+ else {
315
+ // Create a new panel
316
+ const panel = vscode.window.createWebviewPanel('nornResponse', `Running: ${sequenceName}`, column, {
317
+ enableScripts: true,
318
+ retainContextWhenHidden: true,
319
+ });
320
+ ResponsePanel.currentPanel = new ResponsePanel(panel);
321
+ }
322
+ ResponsePanel.currentPanel._sequenceName = sequenceName;
323
+ ResponsePanel.currentPanel._totalSteps = totalSteps;
324
+ ResponsePanel.currentPanel._startTime = Date.now();
325
+ ResponsePanel.currentPanel._sourceFilePath = sourceFilePath || '';
326
+ ResponsePanel.currentPanel._panel.title = `⏳ ${sequenceName}`;
327
+ ResponsePanel.currentPanel._panel.webview.html = ResponsePanel.currentPanel._getStreamingSequenceHtml(sequenceName, totalSteps);
328
+ // Send reset message to clear any previous state (handles retainContextWhenHidden)
329
+ ResponsePanel.currentPanel._panel.webview.postMessage({
330
+ type: 'reset',
331
+ totalSteps: totalSteps
332
+ });
333
+ }
334
+ /**
335
+ * Add a step result to the running sequence display
336
+ */
337
+ static addSequenceStep(stepResult, progressInfo) {
338
+ if (!ResponsePanel.currentPanel) {
339
+ return;
340
+ }
341
+ // Send the step to the webview via postMessage
342
+ ResponsePanel.currentPanel._panel.webview.postMessage({
343
+ type: 'addStep',
344
+ step: stepResult,
345
+ progress: progressInfo,
346
+ elapsed: Date.now() - ResponsePanel.currentPanel._startTime
347
+ });
348
+ }
349
+ /**
350
+ * Finalize the sequence run - update header to show success/failure
351
+ */
352
+ static finalizeSequence(result) {
353
+ if (!ResponsePanel.currentPanel) {
354
+ return;
355
+ }
356
+ ResponsePanel.currentPanel._panel.title = `${result.success ? '✓' : '✗'} ${result.name}`;
357
+ ResponsePanel.currentPanel._panel.webview.postMessage({
358
+ type: 'finalize',
359
+ success: result.success,
360
+ errors: result.errors,
361
+ duration: result.duration
362
+ });
363
+ }
364
+ /**
365
+ * Show the Contract View for schema validation errors
366
+ * Provides a rich UI for navigating validation failures
367
+ */
368
+ static showContractView(extensionUri, data) {
369
+ const column = vscode.ViewColumn.Beside;
370
+ // If we already have a panel, reuse it
371
+ if (ResponsePanel.currentPanel) {
372
+ ResponsePanel.currentPanel._panel.reveal(column);
373
+ }
374
+ else {
375
+ // Create a new panel
376
+ const panel = vscode.window.createWebviewPanel('nornResponse', 'Contract View', column, {
377
+ enableScripts: true,
378
+ retainContextWhenHidden: true,
379
+ });
380
+ ResponsePanel.currentPanel = new ResponsePanel(panel);
381
+ }
382
+ const errorCount = data.errors.length;
383
+ const schemaName = path.basename(data.schemaPath);
384
+ ResponsePanel.currentPanel._panel.title = `⚠ ${errorCount} violation${errorCount !== 1 ? 's' : ''} - ${schemaName}`;
385
+ ResponsePanel.currentPanel._sourceFilePath = data.sourceFile || '';
386
+ ResponsePanel.currentPanel._panel.webview.html = ResponsePanel.currentPanel._getContractViewHtml(data);
387
+ }
388
+ /**
389
+ * Show the Contract Report - a visual comparison of response against schema
390
+ * Shows matched properties with checkmarks and highlights differences
391
+ */
392
+ static showContractReport(extensionUri, data) {
393
+ const column = vscode.ViewColumn.Beside;
394
+ // If we already have a panel, reuse it
395
+ if (ResponsePanel.currentPanel) {
396
+ ResponsePanel.currentPanel._panel.reveal(column);
397
+ }
398
+ else {
399
+ // Create a new panel
400
+ const panel = vscode.window.createWebviewPanel('nornResponse', 'Contract Report', column, {
401
+ enableScripts: true,
402
+ retainContextWhenHidden: true,
403
+ });
404
+ ResponsePanel.currentPanel = new ResponsePanel(panel);
405
+ }
406
+ const errorCount = data.errors.length;
407
+ const schemaName = path.basename(data.schemaPath);
408
+ const titleIcon = errorCount === 0 ? '✓' : '⚠';
409
+ const titleText = errorCount === 0 ? 'All OK' : `${errorCount} issue${errorCount !== 1 ? 's' : ''}`;
410
+ ResponsePanel.currentPanel._panel.title = `${titleIcon} ${titleText} - ${schemaName}`;
411
+ ResponsePanel.currentPanel._sourceFilePath = data.sourceFile || '';
412
+ ResponsePanel.currentPanel._panel.webview.html = ResponsePanel.currentPanel._getContractReportHtml(data);
413
+ }
414
+ _update(method, url, response) {
415
+ this._panel.title = `${response.status} ${method} Response`;
416
+ this._panel.webview.html = this._getHtmlContent(method, url, response);
417
+ }
418
+ _updateSequence(result) {
419
+ this._panel.title = `${result.success ? '✓' : '✗'} ${result.name}`;
420
+ this._panel.webview.html = this._getSequenceHtmlContent(result);
421
+ }
422
+ _updateMultiSequence(sequenceName, runs) {
423
+ const passedRuns = runs.filter(r => r.result.success).length;
424
+ const allPassed = passedRuns === runs.length;
425
+ this._panel.title = `${allPassed ? '✓' : '✗'} ${sequenceName} (${runs.length} runs)`;
426
+ this._panel.webview.html = this._getMultiSequenceHtmlContent(sequenceName, runs);
427
+ }
428
+ /**
429
+ * Get the HTML for streaming sequence results
430
+ */
431
+ _getStreamingSequenceHtml(sequenceName, totalSteps) {
432
+ return /*html*/ `
433
+ <!DOCTYPE html>
434
+ <html lang="en">
435
+ <head>
436
+ <meta charset="UTF-8">
437
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
438
+ <title>Sequence Result</title>
439
+ <style>
440
+ * { box-sizing: border-box; }
441
+
442
+ body {
443
+ font-family: var(--vscode-font-family);
444
+ font-size: var(--vscode-font-size);
445
+ color: var(--vscode-foreground);
446
+ background-color: var(--vscode-editor-background);
447
+ padding: 0;
448
+ margin: 0;
449
+ }
450
+
451
+ .container { padding: 16px; }
452
+
453
+ .sequence-header {
454
+ display: flex;
455
+ align-items: center;
456
+ gap: 16px;
457
+ padding: 16px;
458
+ background: var(--vscode-editor-inactiveSelectionBackground);
459
+ border-radius: 6px;
460
+ margin-bottom: 16px;
461
+ }
462
+
463
+ .sequence-name {
464
+ font-size: 1.2em;
465
+ font-weight: bold;
466
+ }
467
+
468
+ .sequence-status {
469
+ padding: 4px 12px;
470
+ border-radius: 4px;
471
+ font-weight: bold;
472
+ }
473
+
474
+ .status-running { background: #569cd6; color: #fff; }
475
+ .status-success { background: #4ec9b0; color: #000; }
476
+ .status-warning { background: #cca700; color: #000; }
477
+ .status-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
478
+ .status-redirect { background: #dcdcaa; color: #000; }
479
+ .status-client-error { background: #ce9178; color: #000; }
480
+ .status-server-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
481
+
482
+ .meta { color: var(--vscode-descriptionForeground); }
483
+
484
+ .progress-info {
485
+ display: flex;
486
+ align-items: center;
487
+ gap: 8px;
488
+ }
489
+
490
+ .spinner {
491
+ width: 16px;
492
+ height: 16px;
493
+ border: 2px solid var(--vscode-descriptionForeground);
494
+ border-top-color: #569cd6;
495
+ border-radius: 50%;
496
+ animation: spin 1s linear infinite;
497
+ }
498
+
499
+ @keyframes spin {
500
+ to { transform: rotate(360deg); }
501
+ }
502
+
503
+ /* Contract View button for schema validation failures */
504
+ .contract-view-actions {
505
+ margin-top: 12px;
506
+ }
507
+ .contract-view-btn {
508
+ padding: 8px 16px;
509
+ font-size: 13px;
510
+ font-weight: 500;
511
+ border: 1px solid var(--vscode-button-border, var(--vscode-focusBorder));
512
+ background-color: var(--vscode-button-secondaryBackground);
513
+ color: var(--vscode-button-secondaryForeground);
514
+ border-radius: 4px;
515
+ cursor: pointer;
516
+ transition: background-color 0.15s;
517
+ }
518
+ .contract-view-btn:hover {
519
+ background-color: var(--vscode-button-secondaryHoverBackground);
520
+ }
521
+
522
+ .response-item {
523
+ border: 1px solid var(--vscode-panel-border);
524
+ border-radius: 6px;
525
+ margin-bottom: 8px;
526
+ overflow: hidden;
527
+ animation: slideIn 0.2s ease-out;
528
+ }
529
+
530
+ @keyframes slideIn {
531
+ from {
532
+ opacity: 0;
533
+ transform: translateY(-10px);
534
+ }
535
+ to {
536
+ opacity: 1;
537
+ transform: translateY(0);
538
+ }
539
+ }
540
+
541
+ .response-header {
542
+ display: flex;
543
+ align-items: center;
544
+ gap: 12px;
545
+ padding: 12px;
546
+ cursor: pointer;
547
+ background: var(--vscode-editor-inactiveSelectionBackground);
548
+ }
549
+
550
+ .response-header:hover {
551
+ background: var(--vscode-list-hoverBackground);
552
+ }
553
+
554
+ .response-index {
555
+ font-family: var(--vscode-editor-font-family);
556
+ font-weight: bold;
557
+ color: var(--vscode-symbolIcon-variableForeground, #9cdcfe);
558
+ }
559
+
560
+ .status-code {
561
+ padding: 2px 8px;
562
+ border-radius: 3px;
563
+ font-size: 0.9em;
564
+ }
565
+
566
+ .expand-icon {
567
+ margin-left: auto;
568
+ transition: transform 0.2s;
569
+ }
570
+
571
+ .expand-icon.expanded {
572
+ transform: rotate(90deg);
573
+ }
574
+
575
+ .response-body {
576
+ padding: 12px;
577
+ border-top: 1px solid var(--vscode-panel-border);
578
+ }
579
+
580
+ pre {
581
+ margin: 0;
582
+ background: transparent;
583
+ font-family: var(--vscode-editor-font-family);
584
+ font-size: var(--vscode-editor-font-size);
585
+ line-height: 1.5;
586
+ overflow-x: auto;
587
+ }
588
+
589
+ pre code { background: none; }
590
+
591
+ .errors-section {
592
+ margin-top: 16px;
593
+ padding: 12px 16px;
594
+ background: linear-gradient(135deg, rgba(201, 85, 90, 0.2) 0%, rgba(168, 67, 71, 0.15) 100%);
595
+ border-radius: 6px;
596
+ border-left: 3px solid #c9555a;
597
+ }
598
+
599
+ .errors-section h3 { margin-top: 0; color: var(--vscode-foreground); }
600
+ .errors-section ul { margin: 0; padding-left: 20px; }
601
+ .errors-section .error { color: var(--vscode-foreground); white-space: pre-wrap; line-height: 1.5; }
602
+ .errors-section .warning { color: #cca700; }
603
+
604
+ /* JSON Syntax Highlighting */
605
+ .json-key { color: #9cdcfe; background: none; }
606
+ .json-string { color: #ce9178; background: none; }
607
+ .json-number { color: #b5cea8; background: none; }
608
+ .json-boolean { color: #569cd6; background: none; }
609
+ .json-null { color: #569cd6; background: none; }
610
+
611
+ /* Clickable JSON values */
612
+ .json-clickable {
613
+ cursor: pointer;
614
+ border-radius: 3px;
615
+ padding: 1px 3px;
616
+ margin: -1px -3px;
617
+ position: relative;
618
+ transition: background-color 0.15s, box-shadow 0.15s;
619
+ }
620
+ .json-clickable:hover {
621
+ background: var(--vscode-editor-selectionBackground);
622
+ box-shadow: 0 0 0 1px var(--vscode-focusBorder);
623
+ }
624
+ .json-clickable::after {
625
+ content: '+';
626
+ position: absolute;
627
+ right: -16px;
628
+ top: 50%;
629
+ transform: translateY(-50%);
630
+ font-size: 10px;
631
+ font-weight: bold;
632
+ color: var(--vscode-button-background);
633
+ background: var(--vscode-button-foreground);
634
+ border-radius: 50%;
635
+ width: 14px;
636
+ height: 14px;
637
+ line-height: 14px;
638
+ text-align: center;
639
+ opacity: 0;
640
+ transition: opacity 0.15s;
641
+ }
642
+ .json-clickable:hover::after {
643
+ opacity: 1;
644
+ }
645
+
646
+ /* Script Result Styling */
647
+ .script-item { border-left: 3px solid #dcdcaa; }
648
+ .script-icon { font-size: 1.1em; }
649
+ .script-type {
650
+ font-family: var(--vscode-editor-font-family);
651
+ padding: 2px 8px;
652
+ background: #dcdcaa22;
653
+ border-radius: 3px;
654
+ color: #dcdcaa;
655
+ font-size: 0.85em;
656
+ text-transform: uppercase;
657
+ }
658
+ .script-path {
659
+ font-family: var(--vscode-editor-font-family);
660
+ color: var(--vscode-descriptionForeground);
661
+ }
662
+ .capture-var {
663
+ font-family: var(--vscode-editor-font-family);
664
+ color: #9cdcfe;
665
+ font-weight: bold;
666
+ }
667
+ .script-output {
668
+ margin-bottom: 12px;
669
+ }
670
+ .script-output:last-child { margin-bottom: 0; }
671
+ .output-label {
672
+ font-size: 0.85em;
673
+ color: var(--vscode-descriptionForeground);
674
+ margin-bottom: 4px;
675
+ text-transform: uppercase;
676
+ }
677
+ .script-output.stderr .output-label { color: #f14c4c; }
678
+ .script-output.stderr pre { color: #f14c4c; }
679
+
680
+ .step-number {
681
+ font-family: var(--vscode-editor-font-family);
682
+ font-weight: bold;
683
+ color: var(--vscode-descriptionForeground);
684
+ min-width: 24px;
685
+ }
686
+
687
+ /* Print styling */
688
+ .print-item {
689
+ border-left: 3px solid #569cd6;
690
+ }
691
+ .print-item .response-header {
692
+ background: var(--vscode-editor-inactiveSelectionBackground);
693
+ }
694
+ .print-content {
695
+ display: flex;
696
+ align-items: center;
697
+ gap: 12px;
698
+ padding: 12px;
699
+ background: var(--vscode-editor-inactiveSelectionBackground);
700
+ }
701
+ .print-icon {
702
+ font-size: 1em;
703
+ }
704
+ .print-title {
705
+ font-family: var(--vscode-editor-font-family);
706
+ color: #569cd6;
707
+ flex: 1;
708
+ }
709
+ .print-body {
710
+ background: var(--vscode-editor-background);
711
+ }
712
+ .print-body pre {
713
+ margin: 0;
714
+ white-space: pre-wrap;
715
+ word-wrap: break-word;
716
+ }
717
+
718
+ /* Assertion styling */
719
+ .assertion-item {
720
+ border-left: 3px solid var(--vscode-descriptionForeground);
721
+ }
722
+ .assertion-item.assertion-passed {
723
+ border-left-color: #4ec9b0;
724
+ }
725
+ .assertion-item.assertion-failed {
726
+ border-left-color: #f14c4c;
727
+ }
728
+ .assertion-icon {
729
+ font-size: 1.1em;
730
+ font-weight: bold;
731
+ }
732
+ .assertion-icon.status-success { color: #4ec9b0; }
733
+ .assertion-icon.status-error { color: #f14c4c; }
734
+ .assertion-expr {
735
+ font-family: var(--vscode-editor-font-family);
736
+ flex: 1;
737
+ }
738
+ .assertion-expr code {
739
+ background: var(--vscode-textCodeBlock-background);
740
+ padding: 2px 6px;
741
+ border-radius: 3px;
742
+ }
743
+ .assertion-body {
744
+ padding: 12px 16px;
745
+ background: var(--vscode-editor-background);
746
+ }
747
+ .assertion-message {
748
+ color: var(--vscode-descriptionForeground);
749
+ margin-bottom: 8px;
750
+ font-style: italic;
751
+ }
752
+ .assertion-details {
753
+ font-family: var(--vscode-editor-font-family);
754
+ font-size: 0.9em;
755
+ }
756
+ .assertion-expected, .assertion-actual {
757
+ margin: 4px 0;
758
+ }
759
+ .assertion-expected code, .assertion-actual code {
760
+ background: var(--vscode-textCodeBlock-background);
761
+ padding: 2px 6px;
762
+ border-radius: 3px;
763
+ }
764
+ .assertion-error {
765
+ color: #f14c4c;
766
+ font-family: var(--vscode-editor-font-family);
767
+ }
768
+ .response-header.no-expand {
769
+ cursor: default;
770
+ }
771
+ .response-header.no-expand:hover {
772
+ background: var(--vscode-editor-inactiveSelectionBackground);
773
+ }
774
+
775
+ /* Inline response preview for failed assertions */
776
+ .assertion-response-preview {
777
+ margin-top: 12px;
778
+ border: 1px solid var(--vscode-panel-border);
779
+ border-radius: 4px;
780
+ overflow: hidden;
781
+ }
782
+ .assertion-response-preview .preview-header {
783
+ display: flex;
784
+ align-items: center;
785
+ gap: 8px;
786
+ padding: 8px 12px;
787
+ background: var(--vscode-editor-inactiveSelectionBackground);
788
+ cursor: pointer;
789
+ font-family: var(--vscode-editor-font-family);
790
+ font-size: 0.9em;
791
+ }
792
+ .assertion-response-preview .preview-header:hover {
793
+ background: var(--vscode-list-hoverBackground);
794
+ }
795
+ .assertion-response-preview .preview-body {
796
+ max-height: 300px;
797
+ overflow: auto;
798
+ background: var(--vscode-editor-background);
799
+ padding: 8px 12px;
800
+ }
801
+ .assertion-response-preview .preview-body pre {
802
+ margin: 0;
803
+ font-size: 0.85em;
804
+ }
805
+
806
+ /* JSON path highlighting */
807
+ .json-highlighted {
808
+ background: rgba(255, 200, 0, 0.25);
809
+ border-radius: 2px;
810
+ padding: 1px 2px;
811
+ }
812
+
813
+ .step-description {
814
+ color: var(--vscode-descriptionForeground);
815
+ font-family: var(--vscode-editor-font-family);
816
+ font-size: 0.9em;
817
+ }
818
+
819
+ /* Sub-sequence grouping */
820
+ .sequence-group {
821
+ border: 1px solid var(--vscode-panel-border);
822
+ border-radius: 6px;
823
+ margin-bottom: 8px;
824
+ overflow: hidden;
825
+ animation: slideIn 0.2s ease-out;
826
+ }
827
+ .sequence-group-header {
828
+ display: flex;
829
+ align-items: center;
830
+ gap: 12px;
831
+ padding: 10px 12px;
832
+ cursor: pointer;
833
+ background: linear-gradient(135deg, rgba(86, 156, 214, 0.15) 0%, rgba(78, 201, 176, 0.1) 100%);
834
+ border-bottom: 1px solid var(--vscode-panel-border);
835
+ width: 100%;
836
+ box-sizing: border-box;
837
+ }
838
+ .sequence-group-header .expand-icon {
839
+ margin-left: 0;
840
+ }
841
+ .sequence-group-header:hover {
842
+ background: linear-gradient(135deg, rgba(86, 156, 214, 0.25) 0%, rgba(78, 201, 176, 0.15) 100%);
843
+ }
844
+ .sequence-group-icon {
845
+ font-size: 1em;
846
+ }
847
+ .sequence-group-name {
848
+ font-family: var(--vscode-editor-font-family);
849
+ font-weight: bold;
850
+ color: #569cd6;
851
+ }
852
+ .sequence-group-status {
853
+ font-size: 0.85em;
854
+ padding: 2px 8px;
855
+ border-radius: 3px;
856
+ background: rgba(86, 156, 214, 0.2);
857
+ color: #569cd6;
858
+ margin-left: auto;
859
+ }
860
+ .sequence-group-status.running {
861
+ background: rgba(86, 156, 214, 0.2);
862
+ color: #569cd6;
863
+ }
864
+ .sequence-group-status.completed {
865
+ background: rgba(78, 201, 176, 0.2);
866
+ color: #4ec9b0;
867
+ }
868
+ .sequence-group-content {
869
+ padding: 8px;
870
+ background: var(--vscode-editor-background);
871
+ }
872
+ .sequence-group-content .response-item {
873
+ margin-bottom: 6px;
874
+ }
875
+ .sequence-group-content .response-item:last-child {
876
+ margin-bottom: 0;
877
+ }
878
+
879
+ .toggle-all-btn {
880
+ background: var(--vscode-button-secondaryBackground);
881
+ color: var(--vscode-button-secondaryForeground);
882
+ border: none;
883
+ padding: 4px 10px;
884
+ border-radius: 4px;
885
+ cursor: pointer;
886
+ font-size: 0.85em;
887
+ margin-left: auto;
888
+ }
889
+ .toggle-all-btn:hover {
890
+ background: var(--vscode-button-secondaryHoverBackground);
891
+ }
892
+
893
+ /* Sequence Request Details */
894
+ .seq-request-url {
895
+ padding: 6px 12px;
896
+ background: var(--vscode-textCodeBlock-background);
897
+ border-bottom: 1px solid var(--vscode-panel-border);
898
+ font-family: var(--vscode-editor-font-family);
899
+ font-size: 0.9em;
900
+ color: var(--vscode-foreground);
901
+ display: flex;
902
+ align-items: center;
903
+ gap: 8px;
904
+ }
905
+ .seq-request-url .method {
906
+ font-weight: bold;
907
+ padding: 2px 6px;
908
+ border-radius: 3px;
909
+ font-size: 0.85em;
910
+ }
911
+ .seq-request-url .method.GET { background: #4ec9b022; color: #4ec9b0; }
912
+ .seq-request-url .method.POST { background: #dcdcaa22; color: #dcdcaa; }
913
+ .seq-request-url .method.PUT { background: #569cd622; color: #569cd6; }
914
+ .seq-request-url .method.PATCH { background: #c586c022; color: #c586c0; }
915
+ .seq-request-url .method.DELETE { background: #f14c4c22; color: #f14c4c; }
916
+ .seq-tabs {
917
+ display: flex;
918
+ gap: 0;
919
+ padding: 0 12px;
920
+ background: var(--vscode-editor-inactiveSelectionBackground);
921
+ border-bottom: 1px solid var(--vscode-panel-border);
922
+ }
923
+ .seq-tab {
924
+ padding: 6px 12px;
925
+ background: transparent;
926
+ border: none;
927
+ color: var(--vscode-foreground);
928
+ cursor: pointer;
929
+ font-size: 0.85em;
930
+ border-bottom: 2px solid transparent;
931
+ transition: all 0.15s;
932
+ }
933
+ .seq-tab:hover {
934
+ background: var(--vscode-list-hoverBackground);
935
+ }
936
+ .seq-tab.active {
937
+ border-bottom-color: var(--vscode-focusBorder);
938
+ color: var(--vscode-textLink-foreground);
939
+ }
940
+ .seq-tab-content {
941
+ display: none;
942
+ padding: 12px;
943
+ }
944
+ .seq-tab-content.active {
945
+ display: block;
946
+ }
947
+ .seq-body-container {
948
+ position: relative;
949
+ }
950
+ .seq-copy-btn {
951
+ position: absolute;
952
+ top: 0;
953
+ right: 0;
954
+ background: var(--vscode-button-secondaryBackground);
955
+ color: var(--vscode-button-secondaryForeground);
956
+ border: none;
957
+ padding: 4px 8px;
958
+ cursor: pointer;
959
+ font-size: 0.8em;
960
+ border-radius: 3px;
961
+ z-index: 1;
962
+ }
963
+ .seq-copy-btn:hover {
964
+ background: var(--vscode-button-secondaryHoverBackground);
965
+ }
966
+ .seq-compare-btn {
967
+ position: absolute;
968
+ top: 0;
969
+ right: 60px;
970
+ background: var(--vscode-button-secondaryBackground);
971
+ color: var(--vscode-button-secondaryForeground);
972
+ border: none;
973
+ padding: 4px 8px;
974
+ cursor: pointer;
975
+ font-size: 0.8em;
976
+ border-radius: 3px;
977
+ z-index: 1;
978
+ }
979
+ .seq-compare-btn:hover {
980
+ background: var(--vscode-button-secondaryHoverBackground);
981
+ }
982
+ .seq-compare-btn.comparing {
983
+ background: var(--vscode-inputValidation-infoBackground);
984
+ border: 1px solid var(--vscode-inputValidation-infoBorder);
985
+ }
986
+ .norn-toast {
987
+ position: fixed;
988
+ bottom: 20px;
989
+ left: 50%;
990
+ transform: translateX(-50%);
991
+ background: var(--vscode-notifications-background);
992
+ color: var(--vscode-notifications-foreground);
993
+ border: 1px solid var(--vscode-notifications-border);
994
+ padding: 10px 20px;
995
+ border-radius: 4px;
996
+ z-index: 1000;
997
+ opacity: 0;
998
+ transition: opacity 0.3s;
999
+ pointer-events: none;
1000
+ }
1001
+ .norn-toast.visible {
1002
+ opacity: 1;
1003
+ }
1004
+ .seq-schema-btn {
1005
+ position: absolute;
1006
+ top: 0;
1007
+ right: 140px;
1008
+ background: var(--vscode-button-secondaryBackground);
1009
+ color: var(--vscode-button-secondaryForeground);
1010
+ border: none;
1011
+ padding: 4px 8px;
1012
+ cursor: pointer;
1013
+ font-size: 0.8em;
1014
+ border-radius: 3px;
1015
+ z-index: 1;
1016
+ }
1017
+ .seq-schema-btn:hover {
1018
+ background: var(--vscode-button-secondaryHoverBackground);
1019
+ }
1020
+ .seq-headers-table,
1021
+ .seq-cookies-table {
1022
+ width: 100%;
1023
+ border-collapse: collapse;
1024
+ font-family: var(--vscode-editor-font-family);
1025
+ font-size: 0.9em;
1026
+ }
1027
+ .seq-headers-table th,
1028
+ .seq-headers-table td,
1029
+ .seq-cookies-table th,
1030
+ .seq-cookies-table td {
1031
+ padding: 6px 8px;
1032
+ text-align: left;
1033
+ border-bottom: 1px solid var(--vscode-panel-border);
1034
+ }
1035
+ .seq-headers-table th,
1036
+ .seq-cookies-table th {
1037
+ background: var(--vscode-editor-inactiveSelectionBackground);
1038
+ color: var(--vscode-descriptionForeground);
1039
+ font-weight: normal;
1040
+ font-size: 0.85em;
1041
+ }
1042
+ .seq-no-data {
1043
+ color: var(--vscode-descriptionForeground);
1044
+ font-style: italic;
1045
+ padding: 12px;
1046
+ }
1047
+ </style>
1048
+ </head>
1049
+ <body>
1050
+ <div class="container">
1051
+ <div class="sequence-header" id="sequence-header">
1052
+ <span class="sequence-status status-running" id="status-badge">RUNNING</span>
1053
+ <span class="sequence-name">${this._escapeHtml(sequenceName)}</span>
1054
+ <span class="meta" id="step-count">0/${totalSteps} steps</span>
1055
+ <span class="meta" id="elapsed-time">⏱ 0ms</span>
1056
+ <button class="toggle-all-btn" id="toggle-all-btn" onclick="toggleAll()">Expand All</button>
1057
+ <div class="progress-info" id="spinner">
1058
+ <div class="spinner"></div>
1059
+ </div>
1060
+ </div>
1061
+
1062
+ <div class="steps-list" id="steps-list">
1063
+ <!-- Steps will be added here dynamically -->
1064
+ </div>
1065
+
1066
+ <div class="errors-section" id="errors-section" style="display: none;">
1067
+ <h3>Errors & Warnings</h3>
1068
+ <ul id="errors-list"></ul>
1069
+ </div>
1070
+
1071
+ <div id="norn-toast" class="norn-toast"></div>
1072
+ </div>
1073
+
1074
+ <script>
1075
+ const vscode = acquireVsCodeApi();
1076
+ let stepCount = 0;
1077
+ let requestCounter = 0;
1078
+ let allExpanded = false;
1079
+ const totalSteps = ${totalSteps};
1080
+
1081
+ // Track sequence group stack for nested sequences
1082
+ let sequenceStack = [];
1083
+ let sequenceGroupCounter = 0;
1084
+
1085
+ // Response comparison state
1086
+ let compareBaseline = null;
1087
+
1088
+ function showToast(message) {
1089
+ const toast = document.getElementById('norn-toast');
1090
+ toast.textContent = message;
1091
+ toast.classList.add('visible');
1092
+ setTimeout(() => toast.classList.remove('visible'), 3000);
1093
+ }
1094
+
1095
+ function compareResponse(stepId) {
1096
+ const bodyEl = document.getElementById(stepId + '-body-text');
1097
+ if (!bodyEl) return;
1098
+
1099
+ const body = bodyEl.value || bodyEl.textContent || '';
1100
+
1101
+ if (!compareBaseline) {
1102
+ // First click - set baseline
1103
+ compareBaseline = { stepId, body };
1104
+ showToast('Select another response to compare');
1105
+ // Highlight the button
1106
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
1107
+ const btn = document.querySelector('[data-compare-step="' + stepId + '"]');
1108
+ if (btn) btn.classList.add('comparing');
1109
+ } else {
1110
+ // Second click - send compare request
1111
+ vscode.postMessage({ type: 'setBaseline', body: compareBaseline.body });
1112
+ vscode.postMessage({ type: 'compare', body: body });
1113
+ compareBaseline = null;
1114
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
1115
+ }
1116
+ }
1117
+
1118
+ // Listen for reset compare message from extension
1119
+ window.addEventListener('message', event => {
1120
+ const message = event.data;
1121
+ if (message.type === 'resetCompare') {
1122
+ compareBaseline = null;
1123
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
1124
+ }
1125
+ });
1126
+
1127
+ function assertSchema(stepId, method, url, variableName) {
1128
+ const bodyEl = document.getElementById(stepId + '-body-text');
1129
+ if (!bodyEl) return;
1130
+
1131
+ const body = bodyEl.value || bodyEl.textContent || '';
1132
+ vscode.postMessage({
1133
+ type: 'assertSchema',
1134
+ body: body,
1135
+ method: method,
1136
+ url: url,
1137
+ variableName: variableName
1138
+ });
1139
+ showToast('Generating schema...');
1140
+ }
1141
+
1142
+ function assertProperty(path, value, variableName) {
1143
+ vscode.postMessage({
1144
+ type: 'assertProperty',
1145
+ path: path,
1146
+ value: value,
1147
+ variableName: variableName
1148
+ });
1149
+ showToast('Assertion added for ' + path);
1150
+ }
1151
+
1152
+ // Global store for contract data to avoid escaping issues
1153
+ const contractDataStore = {};
1154
+
1155
+ function openContractView(contractId) {
1156
+ const contractData = contractDataStore[contractId];
1157
+ if (!contractData) {
1158
+ console.error('Contract data not found for ID:', contractId);
1159
+ return;
1160
+ }
1161
+ vscode.postMessage({
1162
+ type: 'openContractView',
1163
+ schemaPath: contractData.schemaPath,
1164
+ errors: contractData.errors,
1165
+ responseBody: contractData.responseBody,
1166
+ schema: contractData.schema
1167
+ });
1168
+ }
1169
+
1170
+ function openContractViewFromButton(button) {
1171
+ const contractDataBase64 = button.getAttribute('data-contract');
1172
+ if (!contractDataBase64) {
1173
+ console.error('No contract data on button');
1174
+ return;
1175
+ }
1176
+ try {
1177
+ const contractData = JSON.parse(decodeURIComponent(atob(contractDataBase64)));
1178
+ vscode.postMessage({
1179
+ type: 'openContractView',
1180
+ schemaPath: contractData.schemaPath,
1181
+ errors: contractData.errors,
1182
+ responseBody: contractData.responseBody,
1183
+ schema: contractData.schema
1184
+ });
1185
+ } catch (e) {
1186
+ console.error('Failed to parse contract data:', e);
1187
+ }
1188
+ }
1189
+
1190
+ function storeContractData(id, data) {
1191
+ contractDataStore[id] = data;
1192
+ }
1193
+
1194
+ function escapeHtml(text) {
1195
+ const div = document.createElement('div');
1196
+ div.textContent = text;
1197
+ return div.innerHTML;
1198
+ }
1199
+
1200
+ function escapeJsString(text) {
1201
+ return String(text || '').replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
1202
+ }
1203
+
1204
+ function getStatusClass(status) {
1205
+ if (status >= 200 && status < 300) return 'status-success';
1206
+ if (status >= 300 && status < 400) return 'status-redirect';
1207
+ if (status >= 400 && status < 500) return 'status-client-error';
1208
+ if (status >= 500) return 'status-server-error';
1209
+ return '';
1210
+ }
1211
+
1212
+ function formatJson(obj) {
1213
+ if (typeof obj !== 'object' || obj === null) {
1214
+ return escapeHtml(String(obj));
1215
+ }
1216
+ try {
1217
+ const json = JSON.stringify(obj, null, 2);
1218
+ return json.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
1219
+ .replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
1220
+ .replace(/: (\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>')
1221
+ .replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
1222
+ .replace(/: (null)/g, ': <span class="json-null">$1</span>');
1223
+ } catch {
1224
+ return escapeHtml(String(obj));
1225
+ }
1226
+ }
1227
+
1228
+ function formatJsonWithHighlight(obj, highlightPath) {
1229
+ if (typeof obj !== 'object' || obj === null) {
1230
+ return escapeHtml(String(obj));
1231
+ }
1232
+ try {
1233
+ const json = JSON.stringify(obj, null, 2);
1234
+ let result = json.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
1235
+ .replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
1236
+ .replace(/: (\\d+\\.?\\d*)/g, ': <span class="json-number">$1</span>')
1237
+ .replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
1238
+ .replace(/: (null)/g, ': <span class="json-null">$1</span>');
1239
+
1240
+ // Highlight the specific path if provided (e.g., "body.status" -> highlight "status")
1241
+ if (highlightPath) {
1242
+ const pathParts = highlightPath.replace(/^body\\.?/, '').split('.');
1243
+ const leafKey = pathParts[pathParts.length - 1];
1244
+ if (leafKey) {
1245
+ // Highlight the key that was checked
1246
+ const keyRegex = new RegExp('(<span class="json-key">"' + leafKey + '"</span>:)', 'g');
1247
+ result = result.replace(keyRegex, '<span class="json-highlighted">$1</span>');
1248
+ }
1249
+ }
1250
+ return result;
1251
+ } catch {
1252
+ return escapeHtml(String(obj));
1253
+ }
1254
+ }
1255
+
1256
+ function formatJsonInteractive(obj, variableName, basePath) {
1257
+ basePath = basePath || 'body';
1258
+
1259
+ function encodeData(val) {
1260
+ // Base64 encode JSON to avoid all escaping issues
1261
+ return btoa(JSON.stringify(val));
1262
+ }
1263
+
1264
+ function renderValue(val, path) {
1265
+ const dataAttrs = 'data-path="' + escapeHtml(path) + '" data-var="' + escapeHtml(variableName) + '"';
1266
+
1267
+ if (val === null) {
1268
+ return '<span class="json-null json-clickable" ' + dataAttrs + ' data-value="' + encodeData(null) + '">null</span>';
1269
+ }
1270
+ if (typeof val === 'boolean') {
1271
+ return '<span class="json-boolean json-clickable" ' + dataAttrs + ' data-value="' + encodeData(val) + '">' + val + '</span>';
1272
+ }
1273
+ if (typeof val === 'number') {
1274
+ return '<span class="json-number json-clickable" ' + dataAttrs + ' data-value="' + encodeData(val) + '">' + val + '</span>';
1275
+ }
1276
+ if (typeof val === 'string') {
1277
+ return '<span class="json-string json-clickable" ' + dataAttrs + ' data-value="' + encodeData(val) + '">"' + escapeHtml(val) + '"</span>';
1278
+ }
1279
+ if (Array.isArray(val)) {
1280
+ if (val.length === 0) return '[]';
1281
+ const items = val.map((item, i) => renderValue(item, path + '[' + i + ']'));
1282
+ return '[\\n' + items.map(item => ' ' + item.split('\\n').join('\\n ')).join(',\\n') + '\\n]';
1283
+ }
1284
+ if (typeof val === 'object') {
1285
+ const keys = Object.keys(val);
1286
+ if (keys.length === 0) return '{}';
1287
+ const pairs = keys.map(k => {
1288
+ const newPath = path + '.' + k;
1289
+ const renderedVal = renderValue(val[k], newPath);
1290
+ return ' <span class="json-key">"' + escapeHtml(k) + '"</span>: ' + renderedVal.split('\\n').join('\\n ');
1291
+ });
1292
+ return '{\\n' + pairs.join(',\\n') + '\\n}';
1293
+ }
1294
+ return escapeHtml(String(val));
1295
+ }
1296
+
1297
+ try {
1298
+ return renderValue(obj, basePath);
1299
+ } catch (e) {
1300
+ console.error('formatJsonInteractive error:', e);
1301
+ return formatJson(obj);
1302
+ }
1303
+ }
1304
+
1305
+ // Event delegation for clickable JSON values
1306
+ document.addEventListener('click', function(e) {
1307
+ const target = e.target;
1308
+ if (target && target.classList && target.classList.contains('json-clickable')) {
1309
+ const path = target.getAttribute('data-path');
1310
+ const varName = target.getAttribute('data-var');
1311
+ const encodedValue = target.getAttribute('data-value');
1312
+ if (path && varName && encodedValue) {
1313
+ try {
1314
+ const value = JSON.parse(atob(encodedValue));
1315
+ assertProperty(path, value, varName);
1316
+ } catch (err) {
1317
+ console.error('Failed to parse clickable JSON value:', err);
1318
+ }
1319
+ }
1320
+ }
1321
+ });
1322
+
1323
+ function toggleResponsePreview(headerEl) {
1324
+ const previewBody = headerEl.nextElementSibling;
1325
+ const icon = headerEl.querySelector('.expand-icon');
1326
+ if (previewBody) {
1327
+ if (previewBody.style.display === 'none') {
1328
+ previewBody.style.display = 'block';
1329
+ if (icon) icon.classList.add('expanded');
1330
+ } else {
1331
+ previewBody.style.display = 'none';
1332
+ if (icon) icon.classList.remove('expanded');
1333
+ }
1334
+ }
1335
+ }
1336
+
1337
+ function formatHeaders(headers) {
1338
+ if (!headers || Object.keys(headers).length === 0) {
1339
+ return '<p class="seq-no-data">No headers</p>';
1340
+ }
1341
+ const rows = Object.entries(headers).map(([key, value]) =>
1342
+ '<tr><td>' + escapeHtml(key) + '</td><td>' + escapeHtml(String(value)) + '</td></tr>'
1343
+ ).join('');
1344
+ return '<table class="seq-headers-table"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody>' + rows + '</tbody></table>';
1345
+ }
1346
+
1347
+ function formatCookies(cookies) {
1348
+ if (!cookies || cookies.length === 0) {
1349
+ return '<p class="seq-no-data">No cookies</p>';
1350
+ }
1351
+ const rows = cookies.map(c =>
1352
+ '<tr><td>' + escapeHtml(c.name) + '</td><td title="' + escapeHtml(c.value) + '">' +
1353
+ escapeHtml(c.value.length > 30 ? c.value.substring(0, 30) + '...' : c.value) +
1354
+ '</td><td>' + escapeHtml(c.domain || '') + '</td><td>' + escapeHtml(c.path || '/') + '</td></tr>'
1355
+ ).join('');
1356
+ return '<table class="seq-cookies-table"><thead><tr><th>Name</th><th>Value</th><th>Domain</th><th>Path</th></tr></thead><tbody>' + rows + '</tbody></table>';
1357
+ }
1358
+
1359
+ function switchTab(stepId, tabName) {
1360
+ const container = document.getElementById(stepId);
1361
+ if (!container) return;
1362
+
1363
+ // Update tab buttons
1364
+ const tabs = container.querySelectorAll('.seq-tab');
1365
+ tabs.forEach(t => {
1366
+ if (t.dataset.tab === tabName) {
1367
+ t.classList.add('active');
1368
+ } else {
1369
+ t.classList.remove('active');
1370
+ }
1371
+ });
1372
+
1373
+ // Update tab contents
1374
+ const contents = container.querySelectorAll('.seq-tab-content');
1375
+ contents.forEach(c => {
1376
+ if (c.id === stepId + '-' + tabName) {
1377
+ c.classList.add('active');
1378
+ } else {
1379
+ c.classList.remove('active');
1380
+ }
1381
+ });
1382
+ }
1383
+
1384
+ function copyResponseBody(stepId) {
1385
+ const bodyEl = document.getElementById(stepId + '-body-text');
1386
+ if (bodyEl) {
1387
+ navigator.clipboard.writeText(bodyEl.value || bodyEl.textContent || '');
1388
+ }
1389
+ }
1390
+
1391
+ function toggleResponse(id) {
1392
+ const body = document.getElementById(id);
1393
+ const icon = document.getElementById('icon-' + id);
1394
+
1395
+ if (!body) return;
1396
+
1397
+ if (body.style.display === 'none') {
1398
+ body.style.display = 'block';
1399
+ if (icon) icon.classList.add('expanded');
1400
+ } else {
1401
+ body.style.display = 'none';
1402
+ if (icon) icon.classList.remove('expanded');
1403
+ }
1404
+ }
1405
+
1406
+ function toggleSequenceGroup(id) {
1407
+ const content = document.getElementById('seq-content-' + id);
1408
+ const icon = document.getElementById('seq-icon-' + id);
1409
+
1410
+ if (!content) return;
1411
+
1412
+ if (content.style.display === 'none') {
1413
+ content.style.display = 'block';
1414
+ if (icon) icon.classList.add('expanded');
1415
+ } else {
1416
+ content.style.display = 'none';
1417
+ if (icon) icon.classList.remove('expanded');
1418
+ }
1419
+ }
1420
+
1421
+ function startSequenceGroup(sequenceName) {
1422
+ const groupId = 'seq-' + (++sequenceGroupCounter);
1423
+ const container = sequenceStack.length > 0
1424
+ ? document.getElementById('seq-content-' + sequenceStack[sequenceStack.length - 1])
1425
+ : document.getElementById('steps-list');
1426
+
1427
+ const html = \`
1428
+ <div class="sequence-group" id="\${groupId}">
1429
+ <div class="sequence-group-header" onclick="toggleSequenceGroup('\${groupId}')">
1430
+ <span class="expand-icon" id="seq-icon-\${groupId}">▶</span>
1431
+ <span class="sequence-group-icon">📦</span>
1432
+ <span class="sequence-group-name">\${escapeHtml(sequenceName)}</span>
1433
+ <span class="sequence-group-status running" id="seq-status-\${groupId}">running...</span>
1434
+ </div>
1435
+ <div class="sequence-group-content" id="seq-content-\${groupId}" style="display: none;">
1436
+ </div>
1437
+ </div>
1438
+ \`;
1439
+
1440
+ container.insertAdjacentHTML('beforeend', html);
1441
+ sequenceStack.push(groupId);
1442
+ }
1443
+
1444
+ function endSequenceGroup() {
1445
+ if (sequenceStack.length > 0) {
1446
+ const groupId = sequenceStack.pop();
1447
+ const statusEl = document.getElementById('seq-status-' + groupId);
1448
+ if (statusEl) {
1449
+ statusEl.textContent = 'completed';
1450
+ statusEl.classList.remove('running');
1451
+ statusEl.classList.add('completed');
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ function getCurrentContainer() {
1457
+ if (sequenceStack.length > 0) {
1458
+ return document.getElementById('seq-content-' + sequenceStack[sequenceStack.length - 1]);
1459
+ }
1460
+ return document.getElementById('steps-list');
1461
+ }
1462
+
1463
+ function toggleAll() {
1464
+ const bodies = document.querySelectorAll('.response-body');
1465
+ const seqContents = document.querySelectorAll('.sequence-group-content');
1466
+ const icons = document.querySelectorAll('.expand-icon');
1467
+ const btn = document.getElementById('toggle-all-btn');
1468
+
1469
+ allExpanded = !allExpanded;
1470
+
1471
+ bodies.forEach(body => {
1472
+ body.style.display = allExpanded ? 'block' : 'none';
1473
+ });
1474
+
1475
+ seqContents.forEach(content => {
1476
+ content.style.display = allExpanded ? 'block' : 'none';
1477
+ });
1478
+
1479
+ icons.forEach(icon => {
1480
+ if (allExpanded) {
1481
+ icon.classList.add('expanded');
1482
+ } else {
1483
+ icon.classList.remove('expanded');
1484
+ }
1485
+ });
1486
+
1487
+ btn.textContent = allExpanded ? 'Collapse All' : 'Expand All';
1488
+ }
1489
+
1490
+ function addStepHtml(step, stepIndex, elapsed, progress) {
1491
+ const container = getCurrentContainer();
1492
+ const stepId = 'step-' + stepIndex + '-' + Date.now();
1493
+
1494
+ let html = '';
1495
+
1496
+ if (step.type === 'print' && step.print) {
1497
+ const hasBody = step.print.body && step.print.body.trim();
1498
+
1499
+ if (hasBody) {
1500
+ html = \`
1501
+ <div class="response-item print-item">
1502
+ <div class="response-header" onclick="toggleResponse('\${stepId}')">
1503
+ <span class="step-number">\${stepIndex + 1}.</span>
1504
+ <span class="print-icon">📝</span>
1505
+ <span class="print-title">\${escapeHtml(step.print.title)}</span>
1506
+ <span class="meta">@ \${elapsed}ms</span>
1507
+ <span class="expand-icon" id="icon-\${stepId}">▶</span>
1508
+ </div>
1509
+ <div class="response-body print-body" id="\${stepId}" style="display: none;">
1510
+ <pre><code>\${escapeHtml(step.print.body)}</code></pre>
1511
+ </div>
1512
+ </div>
1513
+ \`;
1514
+ } else {
1515
+ html = \`
1516
+ <div class="response-item print-item">
1517
+ <div class="print-content">
1518
+ <span class="step-number">\${stepIndex + 1}.</span>
1519
+ <span class="print-icon">📝</span>
1520
+ <span class="print-title">\${escapeHtml(step.print.title)}</span>
1521
+ <span class="meta">@ \${elapsed}ms</span>
1522
+ </div>
1523
+ </div>
1524
+ \`;
1525
+ }
1526
+ } else if (step.type === 'request' && step.response) {
1527
+ requestCounter++;
1528
+ const response = step.response;
1529
+ const statusClass = getStatusClass(response.status);
1530
+ // Show variable name if available, otherwise fall back to $N
1531
+ const responseLabel = step.variableName || ('$' + requestCounter);
1532
+ const bodyContent = typeof response.body === 'object'
1533
+ ? formatJsonInteractive(response.body, responseLabel, 'body')
1534
+ : escapeHtml(String(response.body || ''));
1535
+ const rawBody = typeof response.body === 'object'
1536
+ ? JSON.stringify(response.body, null, 2)
1537
+ : String(response.body || '');
1538
+ const method = step.requestMethod || 'GET';
1539
+ const url = step.requestUrl || '';
1540
+ const headersHtml = formatHeaders(response.headers);
1541
+ const cookiesHtml = formatCookies(response.cookies);
1542
+
1543
+ // Show retry info if request was retried
1544
+ const retryInfo = response.retryInfo && response.retryInfo.attemptsMade > 1
1545
+ ? \`<span class="retry-info">🔄 retried \${response.retryInfo.attemptsMade - 1}x</span>\`
1546
+ : '';
1547
+
1548
+ html = \`
1549
+ <div class="response-item">
1550
+ <div class="response-header" onclick="toggleResponse('\${stepId}')">
1551
+ <span class="step-number">\${stepIndex + 1}.</span>
1552
+ <span class="response-index">\${responseLabel}</span>
1553
+ <span class="status-code \${statusClass}">\${response.status} \${escapeHtml(response.statusText || '')}</span>
1554
+ <span class="meta">⏱ \${response.duration}ms</span>
1555
+ \${retryInfo}
1556
+ <span class="expand-icon" id="icon-\${stepId}">▶</span>
1557
+ </div>
1558
+ <div class="response-body" id="\${stepId}" style="display: none;">
1559
+ <div class="seq-request-url">
1560
+ <span class="method \${method}">\${method}</span>
1561
+ <span>\${escapeHtml(url)}</span>
1562
+ </div>
1563
+ <div class="seq-tabs">
1564
+ <button class="seq-tab active" data-tab="body" onclick="switchTab('\${stepId}', 'body')">Body</button>
1565
+ <button class="seq-tab" data-tab="headers" onclick="switchTab('\${stepId}', 'headers')">Headers</button>
1566
+ <button class="seq-tab" data-tab="cookies" onclick="switchTab('\${stepId}', 'cookies')">Cookies</button>
1567
+ </div>
1568
+ <div id="\${stepId}-body" class="seq-tab-content active">
1569
+ <div class="seq-body-container">
1570
+ <button class="seq-schema-btn" onclick="assertSchema('\${stepId}', '\${escapeJsString(method)}', '\${escapeJsString(url)}', '\${escapeJsString(responseLabel)}')">📐 Assert Schema</button>
1571
+ <button class="seq-compare-btn" data-compare-step="\${stepId}" onclick="compareResponse('\${stepId}')">⇄ Compare</button>
1572
+ <button class="seq-copy-btn" onclick="copyResponseBody('\${stepId}')">📋 Copy</button>
1573
+ <pre><code>\${bodyContent}</code></pre>
1574
+ <textarea id="\${stepId}-body-text" style="display:none;">\${escapeHtml(rawBody)}</textarea>
1575
+ </div>
1576
+ </div>
1577
+ <div id="\${stepId}-headers" class="seq-tab-content">
1578
+ \${headersHtml}
1579
+ </div>
1580
+ <div id="\${stepId}-cookies" class="seq-tab-content">
1581
+ \${cookiesHtml}
1582
+ </div>
1583
+ </div>
1584
+ </div>
1585
+ \`;
1586
+ } else if (step.type === 'script' && step.script) {
1587
+ const script = step.script;
1588
+ const hasStderr = script.error && script.error.trim();
1589
+ const scriptStatusClass = !script.success ? 'status-error' : (hasStderr ? 'status-warning' : 'status-success');
1590
+ const scriptStatusText = !script.success ? 'Exit ' + script.exitCode : (hasStderr ? 'Warning' : 'OK');
1591
+ const hasOutput = (script.output && script.output.trim()) || (script.error && script.error.trim());
1592
+
1593
+ html = \`
1594
+ <div class="response-item script-item">
1595
+ <div class="response-header" onclick="toggleResponse('\${stepId}')">
1596
+ <span class="step-number">\${stepIndex + 1}.</span>
1597
+ <span class="script-icon">⚡</span>
1598
+ <span class="script-type">\${escapeHtml(script.type)}</span>
1599
+ <span class="script-path">\${escapeHtml(script.scriptPath)}</span>
1600
+ \${script.captureVar ? '<span class="capture-var">→ ' + escapeHtml(script.captureVar) + '</span>' : ''}
1601
+ <span class="status-code \${scriptStatusClass}">\${scriptStatusText}</span>
1602
+ <span class="meta">⏱ \${script.duration}ms</span>
1603
+ <span class="expand-icon" id="icon-\${stepId}">\${hasOutput ? '▶' : ''}</span>
1604
+ </div>
1605
+ \${hasOutput ? \`
1606
+ <div class="response-body" id="\${stepId}" style="display: none;">
1607
+ \${script.output && script.output.trim() ? '<div class="script-output"><div class="output-label">stdout:</div><pre><code>' + escapeHtml(script.output) + '</code></pre></div>' : ''}
1608
+ \${script.error && script.error.trim() ? '<div class="script-output stderr"><div class="output-label">stderr:</div><pre><code>' + escapeHtml(script.error) + '</code></pre></div>' : ''}
1609
+ </div>
1610
+ \` : ''}
1611
+ </div>
1612
+ \`;
1613
+ } else if (step.type === 'assertion' && step.assertion) {
1614
+ const assertion = step.assertion;
1615
+ const assertionClass = assertion.passed ? 'status-success' : 'status-error';
1616
+ const assertionIcon = assertion.passed ? '✓' : '✗';
1617
+ const hasDetails = !assertion.passed || assertion.message;
1618
+ const isFailed = !assertion.passed;
1619
+ const isSchemaAssertion = assertion.operator === 'matchesSchema';
1620
+
1621
+ // Build display expression with friendly name when available
1622
+ let displayExpr = assertion.expression;
1623
+ if (assertion.friendlyName && assertion.responseIndex) {
1624
+ // Replace $N with the friendly variable name
1625
+ displayExpr = displayExpr.replace('$' + assertion.responseIndex, assertion.friendlyName);
1626
+ }
1627
+
1628
+ let detailsHtml = '';
1629
+ if (hasDetails) {
1630
+ if (assertion.message) {
1631
+ detailsHtml = '<div class="assertion-message">' + escapeHtml(assertion.message) + '</div>';
1632
+ }
1633
+ if (assertion.error) {
1634
+ detailsHtml += '<div class="assertion-error">' + escapeHtml(assertion.error) + '</div>';
1635
+
1636
+ // Add Contract View button for failed schema assertions
1637
+ if (isSchemaAssertion && isFailed && assertion.schemaErrors) {
1638
+ const contractId = 'contract-' + stepId;
1639
+ const contractData = {
1640
+ schemaPath: assertion.schemaPath || assertion.rightValue,
1641
+ errors: assertion.schemaErrors,
1642
+ responseBody: assertion.leftValue,
1643
+ schema: assertion.schema
1644
+ };
1645
+ // Store contract data using base64 encoding in data attribute to avoid escaping issues
1646
+ const contractDataBase64 = btoa(encodeURIComponent(JSON.stringify(contractData)));
1647
+ detailsHtml += \`
1648
+ <div class="contract-view-actions">
1649
+ <button class="contract-view-btn" data-contract-id="\${contractId}" data-contract="\${contractDataBase64}" onclick="openContractViewFromButton(this)">
1650
+ 📋 View Contract Details
1651
+ </button>
1652
+ </div>
1653
+ \`;
1654
+ }
1655
+ } else if (isFailed) {
1656
+ detailsHtml += '<div class="assertion-details">' +
1657
+ '<div class="assertion-expected">Expected: <code>' + escapeHtml(assertion.rightExpression || assertion.operator) + '</code></div>' +
1658
+ '<div class="assertion-actual">Actual: <code>' + escapeHtml(JSON.stringify(assertion.leftValue)) + '</code></div>' +
1659
+ '</div>';
1660
+
1661
+ // Add inline response preview for failures
1662
+ if (assertion.relatedResponse) {
1663
+ const resp = assertion.relatedResponse;
1664
+ const respStatusClass = getStatusClass(resp.status);
1665
+ const bodyPreview = typeof resp.body === 'object'
1666
+ ? formatJsonWithHighlight(resp.body, assertion.jsonPath)
1667
+ : escapeHtml(String(resp.body || ''));
1668
+ const varLabel = assertion.friendlyName || ('$' + assertion.responseIndex);
1669
+
1670
+ detailsHtml += \`
1671
+ <div class="assertion-response-preview">
1672
+ <div class="preview-header" onclick="toggleResponsePreview(this)">
1673
+ <span class="expand-icon">▶</span>
1674
+ <span>\${varLabel} Response</span>
1675
+ <span class="status-code \${respStatusClass}">\${resp.status}</span>
1676
+ </div>
1677
+ <div class="preview-body" style="display: none;">
1678
+ <pre><code>\${bodyPreview}</code></pre>
1679
+ </div>
1680
+ </div>
1681
+ \`;
1682
+ }
1683
+ }
1684
+ }
1685
+
1686
+ // Auto-expand failed assertions
1687
+ const expandedByDefault = isFailed;
1688
+
1689
+ html = \`
1690
+ <div class="response-item assertion-item \${assertion.passed ? 'assertion-passed' : 'assertion-failed'}">
1691
+ <div class="response-header\${hasDetails ? '' : ' no-expand'}" \${hasDetails ? 'onclick="toggleResponse(\\'' + stepId + '\\')"' : ''}>
1692
+ <span class="step-number">\${stepIndex + 1}.</span>
1693
+ <span class="assertion-icon \${assertionClass}">\${assertionIcon}</span>
1694
+ <span class="assertion-expr">assert <code>\${escapeHtml(displayExpr)}</code></span>
1695
+ \${hasDetails ? '<span class="expand-icon' + (expandedByDefault ? ' expanded' : '') + '" id="icon-' + stepId + '">▶</span>' : ''}
1696
+ </div>
1697
+ \${hasDetails ? \`
1698
+ <div class="response-body assertion-body" id="\${stepId}" style="display: \${expandedByDefault ? 'block' : 'none'};">
1699
+ \${detailsHtml}
1700
+ </div>
1701
+ \` : ''}
1702
+ </div>
1703
+ \`;
1704
+ }
1705
+
1706
+ if (html && container) {
1707
+ container.insertAdjacentHTML('beforeend', html);
1708
+ }
1709
+ stepCount++;
1710
+ document.getElementById('step-count').textContent = stepCount + '/${totalSteps} steps';
1711
+ document.getElementById('elapsed-time').textContent = '⏱ ' + elapsed + 'ms';
1712
+ }
1713
+
1714
+ function finalize(success, errors, duration) {
1715
+ // Update status badge
1716
+ const badge = document.getElementById('status-badge');
1717
+ badge.textContent = success ? 'PASSED' : 'FAILED';
1718
+ badge.className = 'sequence-status ' + (success ? 'status-success' : 'status-error');
1719
+
1720
+ // Hide spinner
1721
+ document.getElementById('spinner').style.display = 'none';
1722
+
1723
+ // Update elapsed time
1724
+ document.getElementById('elapsed-time').textContent = '⏱ ' + duration + 'ms total';
1725
+
1726
+ // Show errors if any
1727
+ if (errors && errors.length > 0) {
1728
+ const errorsSection = document.getElementById('errors-section');
1729
+ const errorsList = document.getElementById('errors-list');
1730
+ errorsSection.style.display = 'block';
1731
+
1732
+ errors.forEach(err => {
1733
+ const li = document.createElement('li');
1734
+ li.className = err.startsWith('Warning') ? 'warning' : 'error';
1735
+ li.textContent = err;
1736
+ errorsList.appendChild(li);
1737
+ });
1738
+ }
1739
+ }
1740
+
1741
+ // Listen for messages from the extension
1742
+ window.addEventListener('message', event => {
1743
+ const message = event.data;
1744
+
1745
+ if (message.type === 'reset') {
1746
+ // Clear all state for a fresh run
1747
+ stepCount = 0;
1748
+ requestCounter = 0;
1749
+ allExpanded = false;
1750
+ sequenceStack = [];
1751
+ sequenceGroupCounter = 0;
1752
+ document.getElementById('steps-list').innerHTML = '';
1753
+ document.getElementById('step-count').textContent = '0/' + message.totalSteps + ' steps';
1754
+ document.getElementById('elapsed-time').textContent = '⏱ 0ms';
1755
+ document.getElementById('status-badge').textContent = 'RUNNING';
1756
+ document.getElementById('status-badge').className = 'sequence-status status-running';
1757
+ document.getElementById('spinner').style.display = 'flex';
1758
+ document.getElementById('toggle-all-btn').textContent = 'Expand All';
1759
+ document.getElementById('errors-section').style.display = 'none';
1760
+ document.getElementById('errors-list').innerHTML = '';
1761
+ } else if (message.type === 'addStep') {
1762
+ const progress = message.progress || {};
1763
+
1764
+ // Handle sequence start/end
1765
+ if (progress.stepType === 'sequenceStart' && progress.sequenceName) {
1766
+ startSequenceGroup(progress.sequenceName);
1767
+ } else if (progress.stepType === 'sequenceEnd') {
1768
+ endSequenceGroup();
1769
+ } else if (message.step) {
1770
+ // Regular step
1771
+ addStepHtml(message.step, message.step.stepIndex, message.elapsed, progress);
1772
+ }
1773
+ } else if (message.type === 'finalize') {
1774
+ finalize(message.success, message.errors, message.duration);
1775
+ }
1776
+ });
1777
+ </script>
1778
+ </body>
1779
+ </html>`;
1780
+ }
1781
+ _getMultiSequenceHtmlContent(sequenceName, runs) {
1782
+ const passedRuns = runs.filter(r => r.result.success).length;
1783
+ const totalDuration = runs.reduce((sum, run) => sum + run.result.duration, 0);
1784
+ const allPassed = passedRuns === runs.length;
1785
+ const statusClass = allPassed ? 'status-success' : 'status-error';
1786
+ const statusText = allPassed ? 'PASSED' : 'FAILED';
1787
+ const runsHtml = runs.map((run, index) => {
1788
+ const runId = `run-${index}`;
1789
+ const runStatusClass = run.result.success ? 'status-success' : 'status-error';
1790
+ const runStatusText = run.result.success ? 'PASSED' : 'FAILED';
1791
+ const stepCount = run.result.steps?.length || run.result.responses.length;
1792
+ const expandedByDefault = !run.result.success || index === 0;
1793
+ const runStepsHtml = run.result.steps && run.result.steps.length > 0
1794
+ ? this._renderOrderedSteps(run.result.steps, `${runId}-step`)
1795
+ : this._renderGroupedResults(run.result, `${runId}-legacy`);
1796
+ const runErrorsHtml = run.result.errors.length > 0 ? `
1797
+ <div class="errors-section">
1798
+ <h3>Errors & Warnings</h3>
1799
+ <ul>
1800
+ ${run.result.errors.map(e => `<li class="${e.startsWith('Warning') ? 'warning' : 'error'}">${this._escapeHtml(e)}</li>`).join('')}
1801
+ </ul>
1802
+ </div>
1803
+ ` : '';
1804
+ return `
1805
+ <div class="run-group">
1806
+ <div class="run-group-header" onclick="toggleRunGroup('${runId}')">
1807
+ <span class="expand-icon ${expandedByDefault ? 'expanded' : ''}" id="icon-${runId}">▶</span>
1808
+ <span class="run-group-name">Run ${index + 1}: ${this._escapeHtml(run.label)}</span>
1809
+ <span class="run-group-meta">${stepCount} step${stepCount !== 1 ? 's' : ''}</span>
1810
+ <span class="status-code ${runStatusClass}">${runStatusText}</span>
1811
+ <span class="run-group-meta">⏱ ${run.result.duration}ms</span>
1812
+ </div>
1813
+ <div class="run-group-content" id="${runId}" style="display: ${expandedByDefault ? 'block' : 'none'};">
1814
+ <div class="steps-list">
1815
+ ${runStepsHtml}
1816
+ </div>
1817
+ ${runErrorsHtml}
1818
+ </div>
1819
+ </div>
1820
+ `;
1821
+ }).join('');
1822
+ return /*html*/ `
1823
+ <!DOCTYPE html>
1824
+ <html lang="en">
1825
+ <head>
1826
+ <meta charset="UTF-8">
1827
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1828
+ <title>Sequence Result</title>
1829
+ <style>
1830
+ * { box-sizing: border-box; }
1831
+
1832
+ body {
1833
+ font-family: var(--vscode-font-family);
1834
+ font-size: var(--vscode-font-size);
1835
+ color: var(--vscode-foreground);
1836
+ background-color: var(--vscode-editor-background);
1837
+ padding: 0;
1838
+ margin: 0;
1839
+ }
1840
+
1841
+ .container { padding: 16px; }
1842
+
1843
+ .sequence-header {
1844
+ display: flex;
1845
+ align-items: center;
1846
+ gap: 16px;
1847
+ padding: 16px;
1848
+ background: var(--vscode-editor-inactiveSelectionBackground);
1849
+ border-radius: 6px;
1850
+ margin-bottom: 16px;
1851
+ }
1852
+
1853
+ .sequence-name {
1854
+ font-size: 1.2em;
1855
+ font-weight: bold;
1856
+ }
1857
+
1858
+ .sequence-status {
1859
+ padding: 4px 12px;
1860
+ border-radius: 4px;
1861
+ font-weight: bold;
1862
+ }
1863
+
1864
+ .status-success { background: #4ec9b0; color: #000; }
1865
+ .status-warning { background: #cca700; color: #000; }
1866
+ .status-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
1867
+ .status-redirect { background: #dcdcaa; color: #000; }
1868
+ .status-client-error { background: #ce9178; color: #000; }
1869
+ .status-server-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
1870
+
1871
+ .meta { color: var(--vscode-descriptionForeground); }
1872
+
1873
+ .run-group {
1874
+ border: 1px solid var(--vscode-panel-border);
1875
+ border-radius: 6px;
1876
+ margin-bottom: 12px;
1877
+ overflow: hidden;
1878
+ }
1879
+ .run-group-header {
1880
+ display: flex;
1881
+ align-items: center;
1882
+ gap: 12px;
1883
+ padding: 10px 12px;
1884
+ cursor: pointer;
1885
+ background: linear-gradient(135deg, rgba(86, 156, 214, 0.15) 0%, rgba(78, 201, 176, 0.1) 100%);
1886
+ border-bottom: 1px solid var(--vscode-panel-border);
1887
+ }
1888
+ .run-group-header:hover {
1889
+ background: linear-gradient(135deg, rgba(86, 156, 214, 0.25) 0%, rgba(78, 201, 176, 0.15) 100%);
1890
+ }
1891
+ .run-group-name {
1892
+ font-family: var(--vscode-editor-font-family);
1893
+ font-weight: bold;
1894
+ color: #569cd6;
1895
+ flex: 1;
1896
+ }
1897
+ .run-group-meta {
1898
+ color: var(--vscode-descriptionForeground);
1899
+ font-size: 0.9em;
1900
+ }
1901
+ .run-group-content {
1902
+ padding: 8px;
1903
+ background: var(--vscode-editor-background);
1904
+ }
1905
+
1906
+ .retry-info {
1907
+ color: #dcdcaa;
1908
+ font-size: 0.85em;
1909
+ }
1910
+
1911
+ .response-item {
1912
+ border: 1px solid var(--vscode-panel-border);
1913
+ border-radius: 6px;
1914
+ margin-bottom: 8px;
1915
+ overflow: hidden;
1916
+ }
1917
+
1918
+ .response-header {
1919
+ display: flex;
1920
+ align-items: center;
1921
+ gap: 12px;
1922
+ padding: 12px;
1923
+ cursor: pointer;
1924
+ background: var(--vscode-editor-inactiveSelectionBackground);
1925
+ }
1926
+
1927
+ .response-header:hover {
1928
+ background: var(--vscode-list-hoverBackground);
1929
+ }
1930
+
1931
+ .response-index {
1932
+ font-family: var(--vscode-editor-font-family);
1933
+ font-weight: bold;
1934
+ color: var(--vscode-symbolIcon-variableForeground, #9cdcfe);
1935
+ }
1936
+
1937
+ .status-code {
1938
+ padding: 2px 8px;
1939
+ border-radius: 3px;
1940
+ font-size: 0.9em;
1941
+ }
1942
+
1943
+ .expand-icon {
1944
+ margin-left: auto;
1945
+ transition: transform 0.2s;
1946
+ }
1947
+
1948
+ .expand-icon.expanded {
1949
+ transform: rotate(90deg);
1950
+ }
1951
+
1952
+ .response-body {
1953
+ padding: 12px;
1954
+ border-top: 1px solid var(--vscode-panel-border);
1955
+ }
1956
+
1957
+ .seq-request-url {
1958
+ padding: 6px 12px;
1959
+ background: var(--vscode-textCodeBlock-background);
1960
+ border-bottom: 1px solid var(--vscode-panel-border);
1961
+ font-family: var(--vscode-editor-font-family);
1962
+ font-size: 0.9em;
1963
+ color: var(--vscode-foreground);
1964
+ display: flex;
1965
+ align-items: center;
1966
+ gap: 8px;
1967
+ margin: -12px -12px 12px -12px;
1968
+ }
1969
+ .seq-request-url .method {
1970
+ font-weight: bold;
1971
+ padding: 2px 6px;
1972
+ border-radius: 3px;
1973
+ font-size: 0.85em;
1974
+ }
1975
+ .seq-request-url .method.GET { background: #4ec9b022; color: #4ec9b0; }
1976
+ .seq-request-url .method.POST { background: #dcdcaa22; color: #dcdcaa; }
1977
+ .seq-request-url .method.PUT { background: #569cd622; color: #569cd6; }
1978
+ .seq-request-url .method.PATCH { background: #c586c022; color: #c586c0; }
1979
+ .seq-request-url .method.DELETE { background: #f14c4c22; color: #f14c4c; }
1980
+
1981
+ .seq-body-container {
1982
+ position: relative;
1983
+ }
1984
+ .seq-copy-btn {
1985
+ position: absolute;
1986
+ top: 0;
1987
+ right: 0;
1988
+ background: var(--vscode-button-secondaryBackground);
1989
+ color: var(--vscode-button-secondaryForeground);
1990
+ border: none;
1991
+ padding: 4px 8px;
1992
+ cursor: pointer;
1993
+ font-size: 0.8em;
1994
+ border-radius: 3px;
1995
+ z-index: 1;
1996
+ }
1997
+ .seq-copy-btn:hover {
1998
+ background: var(--vscode-button-secondaryHoverBackground);
1999
+ }
2000
+ .seq-compare-btn {
2001
+ position: absolute;
2002
+ top: 0;
2003
+ right: 60px;
2004
+ background: var(--vscode-button-secondaryBackground);
2005
+ color: var(--vscode-button-secondaryForeground);
2006
+ border: none;
2007
+ padding: 4px 8px;
2008
+ cursor: pointer;
2009
+ font-size: 0.8em;
2010
+ border-radius: 3px;
2011
+ z-index: 1;
2012
+ }
2013
+ .seq-compare-btn:hover {
2014
+ background: var(--vscode-button-secondaryHoverBackground);
2015
+ }
2016
+ .seq-compare-btn.comparing {
2017
+ background: var(--vscode-inputValidation-infoBackground);
2018
+ border: 1px solid var(--vscode-inputValidation-infoBorder);
2019
+ }
2020
+
2021
+ .norn-toast {
2022
+ position: fixed;
2023
+ bottom: 20px;
2024
+ left: 50%;
2025
+ transform: translateX(-50%);
2026
+ background: var(--vscode-notifications-background);
2027
+ color: var(--vscode-notifications-foreground);
2028
+ border: 1px solid var(--vscode-notifications-border);
2029
+ padding: 10px 20px;
2030
+ border-radius: 4px;
2031
+ z-index: 1000;
2032
+ opacity: 0;
2033
+ transition: opacity 0.3s;
2034
+ pointer-events: none;
2035
+ }
2036
+ .norn-toast.visible {
2037
+ opacity: 1;
2038
+ }
2039
+
2040
+ pre {
2041
+ margin: 0;
2042
+ background: transparent;
2043
+ font-family: var(--vscode-editor-font-family);
2044
+ font-size: var(--vscode-editor-font-size);
2045
+ line-height: 1.5;
2046
+ overflow-x: auto;
2047
+ }
2048
+
2049
+ pre code { background: none; }
2050
+
2051
+ .errors-section {
2052
+ margin-top: 12px;
2053
+ padding: 12px 16px;
2054
+ background: linear-gradient(135deg, rgba(201, 85, 90, 0.2) 0%, rgba(168, 67, 71, 0.15) 100%);
2055
+ border-radius: 6px;
2056
+ border-left: 3px solid #c9555a;
2057
+ }
2058
+
2059
+ .errors-section h3 { margin-top: 0; color: var(--vscode-foreground); }
2060
+ .errors-section ul { margin: 0; padding-left: 20px; }
2061
+ .errors-section .error { color: var(--vscode-foreground); white-space: pre-wrap; line-height: 1.5; }
2062
+ .errors-section .warning { color: #cca700; }
2063
+
2064
+ .json-key { color: #9cdcfe; background: none; }
2065
+ .json-string { color: #ce9178; background: none; }
2066
+ .json-number { color: #b5cea8; background: none; }
2067
+ .json-boolean { color: #569cd6; background: none; }
2068
+ .json-null { color: #569cd6; background: none; }
2069
+
2070
+ .script-item { border-left: 3px solid #dcdcaa; }
2071
+ .script-icon { font-size: 1.1em; }
2072
+ .script-type {
2073
+ font-family: var(--vscode-editor-font-family);
2074
+ padding: 2px 8px;
2075
+ background: #dcdcaa22;
2076
+ border-radius: 3px;
2077
+ color: #dcdcaa;
2078
+ font-size: 0.85em;
2079
+ text-transform: uppercase;
2080
+ }
2081
+ .script-path {
2082
+ font-family: var(--vscode-editor-font-family);
2083
+ color: var(--vscode-descriptionForeground);
2084
+ }
2085
+ .capture-var {
2086
+ font-family: var(--vscode-editor-font-family);
2087
+ color: #9cdcfe;
2088
+ font-weight: bold;
2089
+ }
2090
+ .script-output {
2091
+ margin-bottom: 12px;
2092
+ }
2093
+ .script-output:last-child { margin-bottom: 0; }
2094
+ .output-label {
2095
+ font-size: 0.85em;
2096
+ color: var(--vscode-descriptionForeground);
2097
+ margin-bottom: 4px;
2098
+ text-transform: uppercase;
2099
+ }
2100
+ .script-output.stderr .output-label { color: #f14c4c; }
2101
+ .script-output.stderr pre { color: #f14c4c; }
2102
+
2103
+ .step-number {
2104
+ font-family: var(--vscode-editor-font-family);
2105
+ font-weight: bold;
2106
+ color: var(--vscode-descriptionForeground);
2107
+ min-width: 24px;
2108
+ }
2109
+
2110
+ .print-item {
2111
+ border-left: 3px solid #569cd6;
2112
+ }
2113
+ .print-item .response-header {
2114
+ background: var(--vscode-editor-inactiveSelectionBackground);
2115
+ }
2116
+ .print-content {
2117
+ display: flex;
2118
+ align-items: center;
2119
+ gap: 12px;
2120
+ padding: 12px;
2121
+ background: var(--vscode-editor-inactiveSelectionBackground);
2122
+ }
2123
+ .print-icon {
2124
+ font-size: 1em;
2125
+ }
2126
+ .print-title {
2127
+ font-family: var(--vscode-editor-font-family);
2128
+ color: #569cd6;
2129
+ flex: 1;
2130
+ }
2131
+ .print-body {
2132
+ background: var(--vscode-editor-background);
2133
+ }
2134
+ .print-body pre {
2135
+ margin: 0;
2136
+ white-space: pre-wrap;
2137
+ word-wrap: break-word;
2138
+ }
2139
+
2140
+ .assertion-item {
2141
+ border-left: 3px solid var(--vscode-descriptionForeground);
2142
+ }
2143
+ .assertion-item.assertion-passed {
2144
+ border-left-color: #4ec9b0;
2145
+ }
2146
+ .assertion-item.assertion-failed {
2147
+ border-left-color: #f14c4c;
2148
+ }
2149
+ .assertion-icon {
2150
+ font-size: 1.1em;
2151
+ font-weight: bold;
2152
+ }
2153
+ .assertion-icon.status-success { color: #4ec9b0; }
2154
+ .assertion-icon.status-error { color: #f14c4c; }
2155
+ .assertion-expr {
2156
+ font-family: var(--vscode-editor-font-family);
2157
+ flex: 1;
2158
+ }
2159
+ .assertion-expr code {
2160
+ background: var(--vscode-textCodeBlock-background);
2161
+ padding: 2px 6px;
2162
+ border-radius: 3px;
2163
+ }
2164
+ .assertion-body {
2165
+ padding: 12px 16px;
2166
+ background: var(--vscode-editor-background);
2167
+ }
2168
+ .assertion-message {
2169
+ color: var(--vscode-descriptionForeground);
2170
+ margin-bottom: 8px;
2171
+ font-style: italic;
2172
+ }
2173
+ .assertion-details {
2174
+ font-family: var(--vscode-editor-font-family);
2175
+ font-size: 0.9em;
2176
+ }
2177
+ .assertion-expected, .assertion-actual {
2178
+ margin: 4px 0;
2179
+ }
2180
+ .assertion-expected code, .assertion-actual code {
2181
+ background: var(--vscode-textCodeBlock-background);
2182
+ padding: 2px 6px;
2183
+ border-radius: 3px;
2184
+ }
2185
+ .assertion-error {
2186
+ color: #f14c4c;
2187
+ font-family: var(--vscode-editor-font-family);
2188
+ }
2189
+ .response-header.no-expand {
2190
+ cursor: default;
2191
+ }
2192
+ .response-header.no-expand:hover {
2193
+ background: var(--vscode-editor-inactiveSelectionBackground);
2194
+ }
2195
+ </style>
2196
+ </head>
2197
+ <body>
2198
+ <div class="container">
2199
+ <div class="sequence-header">
2200
+ <span class="sequence-status ${statusClass}">${statusText}</span>
2201
+ <span class="sequence-name">${this._escapeHtml(sequenceName)}</span>
2202
+ <span class="meta">${runs.length} run${runs.length !== 1 ? 's' : ''}</span>
2203
+ <span class="meta">${passedRuns}/${runs.length} passed</span>
2204
+ <span class="meta">⏱ ${totalDuration}ms total</span>
2205
+ </div>
2206
+
2207
+ ${runsHtml}
2208
+
2209
+ <div id="norn-toast" class="norn-toast"></div>
2210
+ </div>
2211
+
2212
+ <script>
2213
+ const vscode = acquireVsCodeApi();
2214
+ let compareBaseline = null;
2215
+
2216
+ function showToast(message) {
2217
+ const toast = document.getElementById('norn-toast');
2218
+ if (!toast) return;
2219
+ toast.textContent = message;
2220
+ toast.classList.add('visible');
2221
+ setTimeout(() => toast.classList.remove('visible'), 3000);
2222
+ }
2223
+
2224
+ function copyResponseBody(stepId) {
2225
+ const bodyEl = document.getElementById(stepId + '-body-text');
2226
+ if (!bodyEl) return;
2227
+ const body = bodyEl.value || bodyEl.textContent || '';
2228
+ navigator.clipboard.writeText(body).then(() => {
2229
+ showToast('Copied response body');
2230
+ }).catch(() => {
2231
+ showToast('Copy failed');
2232
+ });
2233
+ }
2234
+
2235
+ function compareResponse(stepId) {
2236
+ const bodyEl = document.getElementById(stepId + '-body-text');
2237
+ if (!bodyEl) return;
2238
+ const body = bodyEl.value || bodyEl.textContent || '';
2239
+
2240
+ if (!compareBaseline) {
2241
+ compareBaseline = { stepId, body };
2242
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
2243
+ const btn = document.querySelector('[data-compare-step="' + stepId + '"]');
2244
+ if (btn) btn.classList.add('comparing');
2245
+ showToast('Select another response to compare');
2246
+ } else {
2247
+ vscode.postMessage({ type: 'setBaseline', body: compareBaseline.body });
2248
+ vscode.postMessage({ type: 'compare', body: body });
2249
+ compareBaseline = null;
2250
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
2251
+ }
2252
+ }
2253
+
2254
+ function toggleResponse(id) {
2255
+ const body = document.getElementById(id);
2256
+ const icon = document.getElementById('icon-' + id);
2257
+
2258
+ if (!body) return;
2259
+
2260
+ if (body.style.display === 'none') {
2261
+ body.style.display = 'block';
2262
+ if (icon) icon.classList.add('expanded');
2263
+ } else {
2264
+ body.style.display = 'none';
2265
+ if (icon) icon.classList.remove('expanded');
2266
+ }
2267
+ }
2268
+
2269
+ function toggleRunGroup(id) {
2270
+ const body = document.getElementById(id);
2271
+ const icon = document.getElementById('icon-' + id);
2272
+
2273
+ if (!body) return;
2274
+
2275
+ if (body.style.display === 'none') {
2276
+ body.style.display = 'block';
2277
+ if (icon) icon.classList.add('expanded');
2278
+ } else {
2279
+ body.style.display = 'none';
2280
+ if (icon) icon.classList.remove('expanded');
2281
+ }
2282
+ }
2283
+
2284
+ window.addEventListener('message', event => {
2285
+ const message = event.data;
2286
+ if (message.type === 'resetCompare') {
2287
+ compareBaseline = null;
2288
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
2289
+ }
2290
+ });
2291
+ </script>
2292
+ </body>
2293
+ </html>`;
2294
+ }
2295
+ _getSequenceHtmlContent(result) {
2296
+ const statusClass = result.success ? 'status-success' : 'status-error';
2297
+ const statusText = result.success ? 'PASSED' : 'FAILED';
2298
+ // Use ordered steps if available, otherwise fall back to grouped display
2299
+ const stepsHtml = result.steps && result.steps.length > 0
2300
+ ? this._renderOrderedSteps(result.steps)
2301
+ : this._renderGroupedResults(result);
2302
+ const errorsHtml = result.errors.length > 0 ? `
2303
+ <div class="errors-section">
2304
+ <h3>Errors & Warnings</h3>
2305
+ <ul>
2306
+ ${result.errors.map(e => `<li class="${e.startsWith('Warning') ? 'warning' : 'error'}">${this._escapeHtml(e)}</li>`).join('')}
2307
+ </ul>
2308
+ </div>
2309
+ ` : '';
2310
+ return /*html*/ `
2311
+ <!DOCTYPE html>
2312
+ <html lang="en">
2313
+ <head>
2314
+ <meta charset="UTF-8">
2315
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2316
+ <title>Sequence Result</title>
2317
+ <style>
2318
+ * { box-sizing: border-box; }
2319
+
2320
+ body {
2321
+ font-family: var(--vscode-font-family);
2322
+ font-size: var(--vscode-font-size);
2323
+ color: var(--vscode-foreground);
2324
+ background-color: var(--vscode-editor-background);
2325
+ padding: 0;
2326
+ margin: 0;
2327
+ }
2328
+
2329
+ .container { padding: 16px; }
2330
+
2331
+ .sequence-header {
2332
+ display: flex;
2333
+ align-items: center;
2334
+ gap: 16px;
2335
+ padding: 16px;
2336
+ background: var(--vscode-editor-inactiveSelectionBackground);
2337
+ border-radius: 6px;
2338
+ margin-bottom: 16px;
2339
+ }
2340
+
2341
+ .sequence-name {
2342
+ font-size: 1.2em;
2343
+ font-weight: bold;
2344
+ }
2345
+
2346
+ .sequence-status {
2347
+ padding: 4px 12px;
2348
+ border-radius: 4px;
2349
+ font-weight: bold;
2350
+ }
2351
+
2352
+ .status-success { background: #4ec9b0; color: #000; }
2353
+ .status-warning { background: #cca700; color: #000; }
2354
+ .status-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
2355
+ .status-redirect { background: #dcdcaa; color: #000; }
2356
+ .status-client-error { background: #ce9178; color: #000; }
2357
+ .status-server-error { background: linear-gradient(135deg, #c9555a 0%, #a84347 100%); color: #fff; }
2358
+
2359
+ .meta { color: var(--vscode-descriptionForeground); }
2360
+
2361
+ .retry-info {
2362
+ color: #dcdcaa;
2363
+ font-size: 0.85em;
2364
+ }
2365
+
2366
+ .response-item {
2367
+ border: 1px solid var(--vscode-panel-border);
2368
+ border-radius: 6px;
2369
+ margin-bottom: 8px;
2370
+ overflow: hidden;
2371
+ }
2372
+
2373
+ .response-header {
2374
+ display: flex;
2375
+ align-items: center;
2376
+ gap: 12px;
2377
+ padding: 12px;
2378
+ cursor: pointer;
2379
+ background: var(--vscode-editor-inactiveSelectionBackground);
2380
+ }
2381
+
2382
+ .response-header:hover {
2383
+ background: var(--vscode-list-hoverBackground);
2384
+ }
2385
+
2386
+ .response-index {
2387
+ font-family: var(--vscode-editor-font-family);
2388
+ font-weight: bold;
2389
+ color: var(--vscode-symbolIcon-variableForeground, #9cdcfe);
2390
+ }
2391
+
2392
+ .status-code {
2393
+ padding: 2px 8px;
2394
+ border-radius: 3px;
2395
+ font-size: 0.9em;
2396
+ }
2397
+
2398
+ .expand-icon {
2399
+ margin-left: auto;
2400
+ transition: transform 0.2s;
2401
+ }
2402
+
2403
+ .expand-icon.expanded {
2404
+ transform: rotate(90deg);
2405
+ }
2406
+
2407
+ .response-body {
2408
+ padding: 12px;
2409
+ border-top: 1px solid var(--vscode-panel-border);
2410
+ }
2411
+
2412
+ .seq-request-url {
2413
+ padding: 6px 12px;
2414
+ background: var(--vscode-textCodeBlock-background);
2415
+ border-bottom: 1px solid var(--vscode-panel-border);
2416
+ font-family: var(--vscode-editor-font-family);
2417
+ font-size: 0.9em;
2418
+ color: var(--vscode-foreground);
2419
+ display: flex;
2420
+ align-items: center;
2421
+ gap: 8px;
2422
+ margin: -12px -12px 12px -12px;
2423
+ }
2424
+ .seq-request-url .method {
2425
+ font-weight: bold;
2426
+ padding: 2px 6px;
2427
+ border-radius: 3px;
2428
+ font-size: 0.85em;
2429
+ }
2430
+ .seq-request-url .method.GET { background: #4ec9b022; color: #4ec9b0; }
2431
+ .seq-request-url .method.POST { background: #dcdcaa22; color: #dcdcaa; }
2432
+ .seq-request-url .method.PUT { background: #569cd622; color: #569cd6; }
2433
+ .seq-request-url .method.PATCH { background: #c586c022; color: #c586c0; }
2434
+ .seq-request-url .method.DELETE { background: #f14c4c22; color: #f14c4c; }
2435
+
2436
+ .seq-body-container {
2437
+ position: relative;
2438
+ }
2439
+ .seq-copy-btn {
2440
+ position: absolute;
2441
+ top: 0;
2442
+ right: 0;
2443
+ background: var(--vscode-button-secondaryBackground);
2444
+ color: var(--vscode-button-secondaryForeground);
2445
+ border: none;
2446
+ padding: 4px 8px;
2447
+ cursor: pointer;
2448
+ font-size: 0.8em;
2449
+ border-radius: 3px;
2450
+ z-index: 1;
2451
+ }
2452
+ .seq-copy-btn:hover {
2453
+ background: var(--vscode-button-secondaryHoverBackground);
2454
+ }
2455
+ .seq-compare-btn {
2456
+ position: absolute;
2457
+ top: 0;
2458
+ right: 60px;
2459
+ background: var(--vscode-button-secondaryBackground);
2460
+ color: var(--vscode-button-secondaryForeground);
2461
+ border: none;
2462
+ padding: 4px 8px;
2463
+ cursor: pointer;
2464
+ font-size: 0.8em;
2465
+ border-radius: 3px;
2466
+ z-index: 1;
2467
+ }
2468
+ .seq-compare-btn:hover {
2469
+ background: var(--vscode-button-secondaryHoverBackground);
2470
+ }
2471
+ .seq-compare-btn.comparing {
2472
+ background: var(--vscode-inputValidation-infoBackground);
2473
+ border: 1px solid var(--vscode-inputValidation-infoBorder);
2474
+ }
2475
+
2476
+ .norn-toast {
2477
+ position: fixed;
2478
+ bottom: 20px;
2479
+ left: 50%;
2480
+ transform: translateX(-50%);
2481
+ background: var(--vscode-notifications-background);
2482
+ color: var(--vscode-notifications-foreground);
2483
+ border: 1px solid var(--vscode-notifications-border);
2484
+ padding: 10px 20px;
2485
+ border-radius: 4px;
2486
+ z-index: 1000;
2487
+ opacity: 0;
2488
+ transition: opacity 0.3s;
2489
+ pointer-events: none;
2490
+ }
2491
+ .norn-toast.visible {
2492
+ opacity: 1;
2493
+ }
2494
+
2495
+ pre {
2496
+ margin: 0;
2497
+ background: transparent;
2498
+ font-family: var(--vscode-editor-font-family);
2499
+ font-size: var(--vscode-editor-font-size);
2500
+ line-height: 1.5;
2501
+ overflow-x: auto;
2502
+ }
2503
+
2504
+ pre code { background: none; }
2505
+
2506
+ .errors-section {
2507
+ margin-top: 16px;
2508
+ padding: 12px 16px;
2509
+ background: linear-gradient(135deg, rgba(201, 85, 90, 0.2) 0%, rgba(168, 67, 71, 0.15) 100%);
2510
+ border-radius: 6px;
2511
+ border-left: 3px solid #c9555a;
2512
+ }
2513
+
2514
+ .errors-section h3 { margin-top: 0; color: var(--vscode-foreground); }
2515
+ .errors-section ul { margin: 0; padding-left: 20px; }
2516
+ .errors-section .error { color: var(--vscode-foreground); white-space: pre-wrap; line-height: 1.5; }
2517
+ .errors-section .warning { color: #cca700; }
2518
+
2519
+ /* JSON Syntax Highlighting */
2520
+ .json-key { color: #9cdcfe; background: none; }
2521
+ .json-string { color: #ce9178; background: none; }
2522
+ .json-number { color: #b5cea8; background: none; }
2523
+ .json-boolean { color: #569cd6; background: none; }
2524
+ .json-null { color: #569cd6; background: none; }
2525
+
2526
+ /* Script Result Styling */
2527
+ .script-item { border-left: 3px solid #dcdcaa; }
2528
+ .script-icon { font-size: 1.1em; }
2529
+ .script-type {
2530
+ font-family: var(--vscode-editor-font-family);
2531
+ padding: 2px 8px;
2532
+ background: #dcdcaa22;
2533
+ border-radius: 3px;
2534
+ color: #dcdcaa;
2535
+ font-size: 0.85em;
2536
+ text-transform: uppercase;
2537
+ }
2538
+ .script-path {
2539
+ font-family: var(--vscode-editor-font-family);
2540
+ color: var(--vscode-descriptionForeground);
2541
+ }
2542
+ .capture-var {
2543
+ font-family: var(--vscode-editor-font-family);
2544
+ color: #9cdcfe;
2545
+ font-weight: bold;
2546
+ }
2547
+ .script-output {
2548
+ margin-bottom: 12px;
2549
+ }
2550
+ .script-output:last-child { margin-bottom: 0; }
2551
+ .output-label {
2552
+ font-size: 0.85em;
2553
+ color: var(--vscode-descriptionForeground);
2554
+ margin-bottom: 4px;
2555
+ text-transform: uppercase;
2556
+ }
2557
+ .script-output.stderr .output-label { color: #f14c4c; }
2558
+ .script-output.stderr pre { color: #f14c4c; }
2559
+
2560
+ .step-number {
2561
+ font-family: var(--vscode-editor-font-family);
2562
+ font-weight: bold;
2563
+ color: var(--vscode-descriptionForeground);
2564
+ min-width: 24px;
2565
+ }
2566
+
2567
+ /* Print styling */
2568
+ .print-item {
2569
+ border-left: 3px solid #569cd6;
2570
+ }
2571
+ .print-item .response-header {
2572
+ background: var(--vscode-editor-inactiveSelectionBackground);
2573
+ }
2574
+ .print-content {
2575
+ display: flex;
2576
+ align-items: center;
2577
+ gap: 12px;
2578
+ padding: 12px;
2579
+ background: var(--vscode-editor-inactiveSelectionBackground);
2580
+ }
2581
+ .print-icon {
2582
+ font-size: 1em;
2583
+ }
2584
+ .print-title {
2585
+ font-family: var(--vscode-editor-font-family);
2586
+ color: #569cd6;
2587
+ flex: 1;
2588
+ }
2589
+ .print-body {
2590
+ background: var(--vscode-editor-background);
2591
+ }
2592
+ .print-body pre {
2593
+ margin: 0;
2594
+ white-space: pre-wrap;
2595
+ word-wrap: break-word;
2596
+ }
2597
+
2598
+ /* Assertion styling */
2599
+ .assertion-item {
2600
+ border-left: 3px solid var(--vscode-descriptionForeground);
2601
+ }
2602
+ .assertion-item.assertion-passed {
2603
+ border-left-color: #4ec9b0;
2604
+ }
2605
+ .assertion-item.assertion-failed {
2606
+ border-left-color: #f14c4c;
2607
+ }
2608
+ .assertion-icon {
2609
+ font-size: 1.1em;
2610
+ font-weight: bold;
2611
+ }
2612
+ .assertion-icon.status-success { color: #4ec9b0; }
2613
+ .assertion-icon.status-error { color: #f14c4c; }
2614
+ .assertion-expr {
2615
+ font-family: var(--vscode-editor-font-family);
2616
+ flex: 1;
2617
+ }
2618
+ .assertion-expr code {
2619
+ background: var(--vscode-textCodeBlock-background);
2620
+ padding: 2px 6px;
2621
+ border-radius: 3px;
2622
+ }
2623
+ .assertion-body {
2624
+ padding: 12px 16px;
2625
+ background: var(--vscode-editor-background);
2626
+ }
2627
+ .assertion-message {
2628
+ color: var(--vscode-descriptionForeground);
2629
+ margin-bottom: 8px;
2630
+ font-style: italic;
2631
+ }
2632
+ .assertion-details {
2633
+ font-family: var(--vscode-editor-font-family);
2634
+ font-size: 0.9em;
2635
+ }
2636
+ .assertion-expected, .assertion-actual {
2637
+ margin: 4px 0;
2638
+ }
2639
+ .assertion-expected code, .assertion-actual code {
2640
+ background: var(--vscode-textCodeBlock-background);
2641
+ padding: 2px 6px;
2642
+ border-radius: 3px;
2643
+ }
2644
+ .assertion-error {
2645
+ color: #f14c4c;
2646
+ font-family: var(--vscode-editor-font-family);
2647
+ }
2648
+ .response-header.no-expand {
2649
+ cursor: default;
2650
+ }
2651
+ .response-header.no-expand:hover {
2652
+ background: var(--vscode-editor-inactiveSelectionBackground);
2653
+ }
2654
+ </style>
2655
+ </head>
2656
+ <body>
2657
+ <div class="container">
2658
+ <div class="sequence-header">
2659
+ <span class="sequence-status ${statusClass}">${statusText}</span>
2660
+ <span class="sequence-name">${this._escapeHtml(result.name)}</span>
2661
+ <span class="meta">${result.steps?.length || result.responses.length} step${(result.steps?.length || result.responses.length) !== 1 ? 's' : ''}</span>
2662
+ <span class="meta">⏱ ${result.duration}ms total</span>
2663
+ </div>
2664
+
2665
+ <div class="steps-list">
2666
+ ${stepsHtml}
2667
+ </div>
2668
+
2669
+ ${errorsHtml}
2670
+
2671
+ <div id="norn-toast" class="norn-toast"></div>
2672
+ </div>
2673
+
2674
+ <script>
2675
+ const vscode = acquireVsCodeApi();
2676
+ let compareBaseline = null;
2677
+
2678
+ function showToast(message) {
2679
+ const toast = document.getElementById('norn-toast');
2680
+ if (!toast) return;
2681
+ toast.textContent = message;
2682
+ toast.classList.add('visible');
2683
+ setTimeout(() => toast.classList.remove('visible'), 3000);
2684
+ }
2685
+
2686
+ function copyResponseBody(stepId) {
2687
+ const bodyEl = document.getElementById(stepId + '-body-text');
2688
+ if (!bodyEl) return;
2689
+ const body = bodyEl.value || bodyEl.textContent || '';
2690
+ navigator.clipboard.writeText(body).then(() => {
2691
+ showToast('Copied response body');
2692
+ }).catch(() => {
2693
+ showToast('Copy failed');
2694
+ });
2695
+ }
2696
+
2697
+ function compareResponse(stepId) {
2698
+ const bodyEl = document.getElementById(stepId + '-body-text');
2699
+ if (!bodyEl) return;
2700
+ const body = bodyEl.value || bodyEl.textContent || '';
2701
+
2702
+ if (!compareBaseline) {
2703
+ compareBaseline = { stepId, body };
2704
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
2705
+ const btn = document.querySelector('[data-compare-step="' + stepId + '"]');
2706
+ if (btn) btn.classList.add('comparing');
2707
+ showToast('Select another response to compare');
2708
+ } else {
2709
+ vscode.postMessage({ type: 'setBaseline', body: compareBaseline.body });
2710
+ vscode.postMessage({ type: 'compare', body: body });
2711
+ compareBaseline = null;
2712
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
2713
+ }
2714
+ }
2715
+
2716
+ function toggleResponse(id) {
2717
+ const body = document.getElementById(id);
2718
+ const icon = document.getElementById('icon-' + id);
2719
+
2720
+ if (!body) return;
2721
+
2722
+ if (body.style.display === 'none') {
2723
+ body.style.display = 'block';
2724
+ if (icon) icon.classList.add('expanded');
2725
+ } else {
2726
+ body.style.display = 'none';
2727
+ if (icon) icon.classList.remove('expanded');
2728
+ }
2729
+ }
2730
+
2731
+ window.addEventListener('message', event => {
2732
+ const message = event.data;
2733
+ if (message.type === 'resetCompare') {
2734
+ compareBaseline = null;
2735
+ document.querySelectorAll('.seq-compare-btn').forEach(btn => btn.classList.remove('comparing'));
2736
+ }
2737
+ });
2738
+ </script>
2739
+ </body>
2740
+ </html>`;
2741
+ }
2742
+ _renderOrderedSteps(steps, stepIdPrefix = 'step') {
2743
+ let requestCounter = 0;
2744
+ return steps.map((step, index) => {
2745
+ const stepId = `${stepIdPrefix}-${index}`;
2746
+ if (step.type === 'print' && step.print) {
2747
+ const hasBody = step.print.body && step.print.body.trim();
2748
+ if (hasBody) {
2749
+ // Print with expandable body
2750
+ return `
2751
+ <div class="response-item print-item">
2752
+ <div class="response-header" onclick="toggleResponse('${stepId}')">
2753
+ <span class="step-number">${index + 1}.</span>
2754
+ <span class="print-icon">📝</span>
2755
+ <span class="print-title">${this._escapeHtml(step.print.title)}</span>
2756
+ <span class="meta">@ ${step.print.timestamp}ms</span>
2757
+ <span class="expand-icon" id="icon-${stepId}">▶</span>
2758
+ </div>
2759
+ <div class="response-body print-body" id="${stepId}" style="display: none;">
2760
+ <pre><code>${this._escapeHtml(step.print.body)}</code></pre>
2761
+ </div>
2762
+ </div>
2763
+ `;
2764
+ }
2765
+ else {
2766
+ // Print without body - simple display
2767
+ return `
2768
+ <div class="response-item print-item">
2769
+ <div class="print-content">
2770
+ <span class="step-number">${index + 1}.</span>
2771
+ <span class="print-icon">📝</span>
2772
+ <span class="print-title">${this._escapeHtml(step.print.title)}</span>
2773
+ <span class="meta">@ ${step.print.timestamp}ms</span>
2774
+ </div>
2775
+ </div>
2776
+ `;
2777
+ }
2778
+ }
2779
+ else if (step.type === 'request' && step.response) {
2780
+ requestCounter++;
2781
+ const response = step.response;
2782
+ const respStatusClass = this._getStatusClass(response.status);
2783
+ const bodyContent = this._formatBody(response.body);
2784
+ const rawBody = typeof response.body === 'object'
2785
+ ? JSON.stringify(response.body, null, 2)
2786
+ : String(response.body ?? '');
2787
+ const method = step.requestMethod || 'GET';
2788
+ const url = step.requestUrl || '';
2789
+ return `
2790
+ <div class="response-item">
2791
+ <div class="response-header" onclick="toggleResponse('${stepId}')">
2792
+ <span class="step-number">${index + 1}.</span>
2793
+ <span class="response-index">$${requestCounter}</span>
2794
+ <span class="status-code ${respStatusClass}">${response.status} ${response.statusText}</span>
2795
+ <span class="meta">⏱ ${response.duration}ms</span>
2796
+ <span class="expand-icon" id="icon-${stepId}">▶</span>
2797
+ </div>
2798
+ <div class="response-body" id="${stepId}" style="display: none;">
2799
+ <div class="seq-request-url">
2800
+ <span class="method ${method}">${method}</span>
2801
+ <span>${this._escapeHtml(url)}</span>
2802
+ </div>
2803
+ <div class="seq-body-container">
2804
+ <button class="seq-compare-btn" data-compare-step="${stepId}" onclick="compareResponse('${stepId}')">⇄ Compare</button>
2805
+ <button class="seq-copy-btn" onclick="copyResponseBody('${stepId}')">📋 Copy</button>
2806
+ <pre><code>${bodyContent}</code></pre>
2807
+ <textarea id="${stepId}-body-text" style="display:none;">${this._escapeHtml(rawBody)}</textarea>
2808
+ </div>
2809
+ </div>
2810
+ </div>
2811
+ `;
2812
+ }
2813
+ else if (step.type === 'script' && step.script) {
2814
+ const script = step.script;
2815
+ const hasStderr = script.error && script.error.trim();
2816
+ // Show error status if exit code non-zero, or warning if there's stderr output
2817
+ const scriptStatusClass = !script.success ? 'status-error' : (hasStderr ? 'status-warning' : 'status-success');
2818
+ const scriptStatusText = !script.success ? `Exit ${script.exitCode}` : (hasStderr ? 'Warning' : 'OK');
2819
+ const hasOutput = script.output.trim() || script.error.trim();
2820
+ return `
2821
+ <div class="response-item script-item">
2822
+ <div class="response-header" onclick="toggleResponse('${stepId}')">
2823
+ <span class="step-number">${index + 1}.</span>
2824
+ <span class="script-icon">⚡</span>
2825
+ <span class="script-type">${script.type}</span>
2826
+ <span class="script-path">${this._escapeHtml(script.scriptPath)}</span>
2827
+ ${script.captureVar ? `<span class="capture-var">→ ${this._escapeHtml(script.captureVar)}</span>` : ''}
2828
+ <span class="status-code ${scriptStatusClass}">${scriptStatusText}</span>
2829
+ <span class="meta">⏱ ${script.duration}ms</span>
2830
+ <span class="expand-icon" id="icon-${stepId}">${hasOutput ? '▶' : ''}</span>
2831
+ </div>
2832
+ ${hasOutput ? `
2833
+ <div class="response-body" id="${stepId}" style="display: none;">
2834
+ ${script.output.trim() ? `<div class="script-output"><div class="output-label">stdout:</div><pre><code>${this._escapeHtml(script.output)}</code></pre></div>` : ''}
2835
+ ${script.error.trim() ? `<div class="script-output stderr"><div class="output-label">stderr:</div><pre><code>${this._escapeHtml(script.error)}</code></pre></div>` : ''}
2836
+ </div>
2837
+ ` : ''}
2838
+ </div>
2839
+ `;
2840
+ }
2841
+ else if (step.type === 'assertion' && step.assertion) {
2842
+ const assertion = step.assertion;
2843
+ const assertionClass = assertion.passed ? 'status-success' : 'status-error';
2844
+ const assertionIcon = assertion.passed ? '✓' : '✗';
2845
+ const hasDetails = !assertion.passed || assertion.message;
2846
+ let detailsHtml = '';
2847
+ if (hasDetails) {
2848
+ if (assertion.error) {
2849
+ detailsHtml = `<div class="assertion-error">${this._escapeHtml(assertion.error)}</div>`;
2850
+ }
2851
+ else if (!assertion.passed) {
2852
+ detailsHtml = `<div class="assertion-details">` +
2853
+ `<div class="assertion-expected">Expected: <code>${this._escapeHtml(assertion.rightExpression || assertion.operator)}</code></div>` +
2854
+ `<div class="assertion-actual">Actual: <code>${this._escapeHtml(JSON.stringify(assertion.leftValue))}</code></div>` +
2855
+ `</div>`;
2856
+ }
2857
+ if (assertion.message) {
2858
+ detailsHtml = `<div class="assertion-message">${this._escapeHtml(assertion.message)}</div>` + detailsHtml;
2859
+ }
2860
+ }
2861
+ return `
2862
+ <div class="response-item assertion-item ${assertion.passed ? 'assertion-passed' : 'assertion-failed'}">
2863
+ <div class="response-header${hasDetails ? '' : ' no-expand'}"${hasDetails ? ` onclick="toggleResponse('${stepId}')"` : ''}>
2864
+ <span class="step-number">${index + 1}.</span>
2865
+ <span class="assertion-icon ${assertionClass}">${assertionIcon}</span>
2866
+ <span class="assertion-expr">assert <code>${this._escapeHtml(assertion.expression)}</code></span>
2867
+ ${hasDetails ? `<span class="expand-icon" id="icon-${stepId}">▶</span>` : ''}
2868
+ </div>
2869
+ ${hasDetails ? `
2870
+ <div class="response-body assertion-body" id="${stepId}" style="display: none;">
2871
+ ${detailsHtml}
2872
+ </div>
2873
+ ` : ''}
2874
+ </div>
2875
+ `;
2876
+ }
2877
+ return '';
2878
+ }).join('');
2879
+ }
2880
+ _renderGroupedResults(result, idPrefix = 'grouped') {
2881
+ // Fallback for older results without ordered steps
2882
+ const responsesHtml = result.responses.map((response, index) => {
2883
+ const responseId = `${idPrefix}-resp-${index}`;
2884
+ const respStatusClass = this._getStatusClass(response.status);
2885
+ const bodyContent = this._formatBody(response.body);
2886
+ const rawBody = typeof response.body === 'object'
2887
+ ? JSON.stringify(response.body, null, 2)
2888
+ : String(response.body ?? '');
2889
+ return `
2890
+ <div class="response-item">
2891
+ <div class="response-header" onclick="toggleResponse('${responseId}')">
2892
+ <span class="response-index">$${index + 1}</span>
2893
+ <span class="status-code ${respStatusClass}">${response.status} ${response.statusText}</span>
2894
+ <span class="meta">⏱ ${response.duration}ms</span>
2895
+ <span class="expand-icon" id="icon-${responseId}">▶</span>
2896
+ </div>
2897
+ <div class="response-body" id="${responseId}" style="display: none;">
2898
+ <div class="seq-body-container">
2899
+ <button class="seq-compare-btn" data-compare-step="${responseId}" onclick="compareResponse('${responseId}')">⇄ Compare</button>
2900
+ <button class="seq-copy-btn" onclick="copyResponseBody('${responseId}')">📋 Copy</button>
2901
+ <pre><code>${bodyContent}</code></pre>
2902
+ <textarea id="${responseId}-body-text" style="display:none;">${this._escapeHtml(rawBody)}</textarea>
2903
+ </div>
2904
+ </div>
2905
+ </div>
2906
+ `;
2907
+ }).join('');
2908
+ const scriptResultsHtml = result.scriptResults && result.scriptResults.length > 0 ? result.scriptResults.map((script, index) => {
2909
+ const scriptId = `${idPrefix}-script-${index}`;
2910
+ const hasStderr = script.error && script.error.trim();
2911
+ // Show error status if exit code non-zero, or warning if there's stderr output
2912
+ const scriptStatusClass = !script.success ? 'status-error' : (hasStderr ? 'status-warning' : 'status-success');
2913
+ const scriptStatusText = !script.success ? `Exit ${script.exitCode}` : (hasStderr ? 'Warning' : 'OK');
2914
+ const hasOutput = script.output.trim() || script.error.trim();
2915
+ return `
2916
+ <div class="response-item script-item">
2917
+ <div class="response-header" onclick="toggleResponse('${scriptId}')">
2918
+ <span class="script-icon">⚡</span>
2919
+ <span class="script-type">${script.type}</span>
2920
+ <span class="script-path">${this._escapeHtml(script.scriptPath)}</span>
2921
+ ${script.captureVar ? `<span class="capture-var">→ ${this._escapeHtml(script.captureVar)}</span>` : ''}
2922
+ <span class="status-code ${scriptStatusClass}">${scriptStatusText}</span>
2923
+ <span class="meta">⏱ ${script.duration}ms</span>
2924
+ <span class="expand-icon" id="icon-${scriptId}">${hasOutput ? '▶' : ''}</span>
2925
+ </div>
2926
+ ${hasOutput ? `
2927
+ <div class="response-body" id="${scriptId}" style="display: none;">
2928
+ ${script.output.trim() ? `<div class="script-output"><div class="output-label">stdout:</div><pre><code>${this._escapeHtml(script.output)}</code></pre></div>` : ''}
2929
+ ${script.error.trim() ? `<div class="script-output stderr"><div class="output-label">stderr:</div><pre><code>${this._escapeHtml(script.error)}</code></pre></div>` : ''}
2930
+ </div>
2931
+ ` : ''}
2932
+ </div>
2933
+ `;
2934
+ }).join('') : '';
2935
+ return responsesHtml + scriptResultsHtml;
2936
+ }
2937
+ _updateError(method, url, errorMessage) {
2938
+ this._panel.title = `Error - ${method}`;
2939
+ this._panel.webview.html = this._getErrorHtmlContent(method, url, errorMessage);
2940
+ }
2941
+ _getErrorHtmlContent(method, url, errorMessage) {
2942
+ return /*html*/ `
2943
+ <!DOCTYPE html>
2944
+ <html lang="en">
2945
+ <head>
2946
+ <meta charset="UTF-8">
2947
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2948
+ <title>Error</title>
2949
+ <style>
2950
+ * {
2951
+ box-sizing: border-box;
2952
+ }
2953
+
2954
+ body {
2955
+ font-family: var(--vscode-font-family);
2956
+ font-size: var(--vscode-font-size);
2957
+ color: var(--vscode-foreground);
2958
+ background-color: var(--vscode-editor-background);
2959
+ padding: 0;
2960
+ margin: 0;
2961
+ }
2962
+
2963
+ .container {
2964
+ padding: 16px;
2965
+ max-width: 100%;
2966
+ }
2967
+
2968
+ .error-bar {
2969
+ display: flex;
2970
+ align-items: center;
2971
+ gap: 16px;
2972
+ padding: 12px 16px;
2973
+ background: linear-gradient(135deg, #c9555a 0%, #a84347 100%);
2974
+ border: none;
2975
+ border-radius: 6px;
2976
+ margin-bottom: 16px;
2977
+ }
2978
+
2979
+ .error-icon {
2980
+ font-size: 1.5em;
2981
+ filter: brightness(0) invert(1);
2982
+ }
2983
+
2984
+ .error-title {
2985
+ font-weight: bold;
2986
+ color: #ffffff;
2987
+ }
2988
+
2989
+ .request-info {
2990
+ font-family: var(--vscode-editor-font-family);
2991
+ font-size: 0.85em;
2992
+ color: var(--vscode-descriptionForeground);
2993
+ word-break: break-all;
2994
+ margin-bottom: 16px;
2995
+ }
2996
+
2997
+ .method {
2998
+ font-weight: bold;
2999
+ color: var(--vscode-symbolIcon-methodForeground, #dcdcaa);
3000
+ }
3001
+
3002
+ .error-message {
3003
+ background: rgba(201, 85, 90, 0.15);
3004
+ padding: 16px;
3005
+ border-radius: 6px;
3006
+ font-family: var(--vscode-editor-font-family);
3007
+ font-size: var(--vscode-editor-font-size);
3008
+ line-height: 1.6;
3009
+ color: var(--vscode-foreground);
3010
+ border-left: 3px solid #c9555a;
3011
+ white-space: pre-wrap;
3012
+ word-break: break-word;
3013
+ }
3014
+
3015
+ .help-section {
3016
+ margin-top: 24px;
3017
+ padding: 16px;
3018
+ background: var(--vscode-editor-inactiveSelectionBackground);
3019
+ border-radius: 6px;
3020
+ }
3021
+
3022
+ .help-title {
3023
+ font-weight: bold;
3024
+ margin-bottom: 8px;
3025
+ }
3026
+
3027
+ .help-list {
3028
+ margin: 0;
3029
+ padding-left: 20px;
3030
+ }
3031
+
3032
+ .help-list li {
3033
+ margin-bottom: 4px;
3034
+ }
3035
+ </style>
3036
+ </head>
3037
+ <body>
3038
+ <div class="container">
3039
+ <div class="error-bar">
3040
+ <span class="error-icon">⚠️</span>
3041
+ <span class="error-title">Request Failed</span>
3042
+ </div>
3043
+
3044
+ <div class="request-info">
3045
+ <span class="method">${method}</span> ${this._escapeHtml(url)}
3046
+ </div>
3047
+
3048
+ <div class="error-message">
3049
+ ${this._escapeHtml(errorMessage)}
3050
+ </div>
3051
+
3052
+ <div class="help-section">
3053
+ <div class="help-title">Common causes:</div>
3054
+ <ul class="help-list">
3055
+ <li>Invalid or malformed URL</li>
3056
+ <li>Network connectivity issues</li>
3057
+ <li>Server is unreachable or down</li>
3058
+ <li>DNS resolution failed</li>
3059
+ <li>Request timed out</li>
3060
+ <li>SSL/TLS certificate issues</li>
3061
+ </ul>
3062
+ </div>
3063
+ </div>
3064
+ </body>
3065
+ </html>`;
3066
+ }
3067
+ _getHtmlContent(method, url, response) {
3068
+ const statusClass = this._getStatusClass(response.status);
3069
+ const bodyContent = this._formatBody(response.body);
3070
+ const headersHtml = this._formatHeaders(response.headers);
3071
+ const cookiesHtml = this._formatCookies(response.cookies);
3072
+ const bodySize = this._getBodySize(response.body);
3073
+ return /*html*/ `
3074
+ <!DOCTYPE html>
3075
+ <html lang="en">
3076
+ <head>
3077
+ <meta charset="UTF-8">
3078
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3079
+ <title>Response</title>
3080
+ <style>
3081
+ :root {
3082
+ --success-color: #4ec9b0;
3083
+ --redirect-color: #dcdcaa;
3084
+ --client-error-color: #ce9178;
3085
+ --server-error-color: #f14c4c;
3086
+ }
3087
+
3088
+ * {
3089
+ box-sizing: border-box;
3090
+ }
3091
+
3092
+ body {
3093
+ font-family: var(--vscode-font-family);
3094
+ font-size: var(--vscode-font-size);
3095
+ color: var(--vscode-foreground);
3096
+ background-color: var(--vscode-editor-background);
3097
+ padding: 0;
3098
+ margin: 0;
3099
+ }
3100
+
3101
+ .container {
3102
+ padding: 16px;
3103
+ max-width: 100%;
3104
+ }
3105
+
3106
+ .status-bar {
3107
+ display: flex;
3108
+ align-items: center;
3109
+ gap: 16px;
3110
+ padding: 12px 16px;
3111
+ background: var(--vscode-editor-inactiveSelectionBackground);
3112
+ border-radius: 6px;
3113
+ margin-bottom: 16px;
3114
+ flex-wrap: wrap;
3115
+ }
3116
+
3117
+ .status-code {
3118
+ font-weight: bold;
3119
+ font-size: 1.2em;
3120
+ padding: 4px 12px;
3121
+ border-radius: 4px;
3122
+ }
3123
+
3124
+ .status-success { background: var(--success-color); color: #000; }
3125
+ .status-warning { background: #cca700; color: #000; }
3126
+ .status-redirect { background: var(--redirect-color); color: #000; }
3127
+ .status-client-error { background: var(--client-error-color); color: #000; }
3128
+ .status-server-error { background: var(--server-error-color); color: #000; }
3129
+
3130
+ .meta {
3131
+ color: var(--vscode-descriptionForeground);
3132
+ font-size: 0.9em;
3133
+ }
3134
+
3135
+ .request-info {
3136
+ font-family: var(--vscode-editor-font-family);
3137
+ font-size: 0.85em;
3138
+ color: var(--vscode-descriptionForeground);
3139
+ word-break: break-all;
3140
+ }
3141
+
3142
+ .method {
3143
+ font-weight: bold;
3144
+ color: var(--vscode-symbolIcon-methodForeground, #dcdcaa);
3145
+ }
3146
+
3147
+ .tabs {
3148
+ display: flex;
3149
+ gap: 0;
3150
+ border-bottom: 1px solid var(--vscode-panel-border);
3151
+ margin-bottom: 16px;
3152
+ }
3153
+
3154
+ .tab {
3155
+ padding: 8px 16px;
3156
+ cursor: pointer;
3157
+ border: none;
3158
+ background: transparent;
3159
+ color: var(--vscode-foreground);
3160
+ opacity: 0.7;
3161
+ border-bottom: 2px solid transparent;
3162
+ font-size: inherit;
3163
+ font-family: inherit;
3164
+ }
3165
+
3166
+ .tab:hover {
3167
+ opacity: 1;
3168
+ }
3169
+
3170
+ .tab.active {
3171
+ opacity: 1;
3172
+ border-bottom-color: var(--vscode-focusBorder);
3173
+ }
3174
+
3175
+ .tab-content {
3176
+ display: none;
3177
+ }
3178
+
3179
+ .tab-content.active {
3180
+ display: block;
3181
+ }
3182
+
3183
+ .body-container {
3184
+ position: relative;
3185
+ }
3186
+
3187
+ .copy-btn {
3188
+ position: absolute;
3189
+ top: 8px;
3190
+ right: 8px;
3191
+ padding: 6px 12px;
3192
+ background: var(--vscode-button-background);
3193
+ color: var(--vscode-button-foreground);
3194
+ border: none;
3195
+ border-radius: 4px;
3196
+ cursor: pointer;
3197
+ font-size: 0.85em;
3198
+ }
3199
+
3200
+ .copy-btn:hover {
3201
+ background: var(--vscode-button-hoverBackground);
3202
+ }
3203
+
3204
+ .compare-btn {
3205
+ position: absolute;
3206
+ top: 8px;
3207
+ right: 80px;
3208
+ padding: 6px 12px;
3209
+ background: var(--vscode-button-secondaryBackground);
3210
+ color: var(--vscode-button-secondaryForeground);
3211
+ border: none;
3212
+ border-radius: 4px;
3213
+ cursor: pointer;
3214
+ font-size: 0.85em;
3215
+ }
3216
+
3217
+ .compare-btn:hover {
3218
+ background: var(--vscode-button-secondaryHoverBackground);
3219
+ }
3220
+
3221
+ .compare-btn.comparing {
3222
+ background: var(--vscode-inputValidation-infoBackground);
3223
+ border: 1px solid var(--vscode-inputValidation-infoBorder);
3224
+ }
3225
+
3226
+ .norn-toast {
3227
+ position: fixed;
3228
+ bottom: 20px;
3229
+ left: 50%;
3230
+ transform: translateX(-50%);
3231
+ background: var(--vscode-notifications-background);
3232
+ color: var(--vscode-notifications-foreground);
3233
+ border: 1px solid var(--vscode-notifications-border);
3234
+ padding: 10px 20px;
3235
+ border-radius: 4px;
3236
+ z-index: 1000;
3237
+ opacity: 0;
3238
+ transition: opacity 0.3s;
3239
+ pointer-events: none;
3240
+ }
3241
+
3242
+ .norn-toast.visible {
3243
+ opacity: 1;
3244
+ }
3245
+
3246
+ pre {
3247
+ background: transparent;
3248
+ padding: 16px;
3249
+ border-radius: 6px;
3250
+ overflow-x: auto;
3251
+ margin: 0;
3252
+ font-family: var(--vscode-editor-font-family);
3253
+ font-size: var(--vscode-editor-font-size);
3254
+ line-height: 1.5;
3255
+ }
3256
+
3257
+ pre code {
3258
+ background: none;
3259
+ }
3260
+
3261
+ .headers-table {
3262
+ width: 100%;
3263
+ border-collapse: collapse;
3264
+ }
3265
+
3266
+ .headers-table th,
3267
+ .headers-table td {
3268
+ padding: 8px 12px;
3269
+ text-align: left;
3270
+ border-bottom: 1px solid var(--vscode-panel-border);
3271
+ }
3272
+
3273
+ .headers-table th {
3274
+ background: var(--vscode-editor-inactiveSelectionBackground);
3275
+ font-weight: 600;
3276
+ }
3277
+
3278
+ .header-name {
3279
+ color: var(--vscode-symbolIcon-propertyForeground, #9cdcfe);
3280
+ font-family: var(--vscode-editor-font-family);
3281
+ }
3282
+
3283
+ .header-value {
3284
+ color: var(--vscode-foreground);
3285
+ font-family: var(--vscode-editor-font-family);
3286
+ word-break: break-all;
3287
+ }
3288
+
3289
+ /* Cookies */
3290
+ .cookies-table td {
3291
+ font-size: 0.9em;
3292
+ }
3293
+
3294
+ .cookie-name {
3295
+ color: var(--vscode-symbolIcon-propertyForeground, #9cdcfe);
3296
+ font-family: var(--vscode-editor-font-family);
3297
+ }
3298
+
3299
+ .cookie-value {
3300
+ color: var(--vscode-foreground);
3301
+ font-family: var(--vscode-editor-font-family);
3302
+ max-width: 200px;
3303
+ overflow: hidden;
3304
+ text-overflow: ellipsis;
3305
+ cursor: help;
3306
+ }
3307
+
3308
+ .cookie-flags {
3309
+ text-align: center;
3310
+ }
3311
+
3312
+ .cookies-container {
3313
+ padding: 8px 0;
3314
+ }
3315
+
3316
+ .cookie-summary {
3317
+ color: var(--vscode-foreground);
3318
+ font-weight: 500;
3319
+ margin-bottom: 16px;
3320
+ padding: 8px 12px;
3321
+ background: var(--vscode-editor-inactiveSelectionBackground);
3322
+ border-radius: 4px;
3323
+ }
3324
+
3325
+ .cookie-domain-section {
3326
+ margin-bottom: 16px;
3327
+ }
3328
+
3329
+ .cookie-domain-header {
3330
+ color: var(--vscode-foreground);
3331
+ font-size: 1em;
3332
+ margin: 0 0 8px 0;
3333
+ padding: 4px 0;
3334
+ border-bottom: 1px solid var(--vscode-panel-border);
3335
+ }
3336
+
3337
+ .cookie-count {
3338
+ color: var(--vscode-descriptionForeground);
3339
+ font-weight: normal;
3340
+ font-size: 0.9em;
3341
+ }
3342
+
3343
+ .no-cookies {
3344
+ color: var(--vscode-descriptionForeground);
3345
+ font-style: italic;
3346
+ padding: 16px;
3347
+ }
3348
+
3349
+ .cookie-info {
3350
+ color: var(--vscode-descriptionForeground);
3351
+ font-size: 0.9em;
3352
+ margin-top: 16px;
3353
+ padding: 8px;
3354
+ background: var(--vscode-editor-inactiveSelectionBackground);
3355
+ border-radius: 4px;
3356
+ }
3357
+
3358
+ .cookie-info code {
3359
+ background: var(--vscode-textCodeBlock-background);
3360
+ padding: 2px 6px;
3361
+ border-radius: 3px;
3362
+ }
3363
+
3364
+ /* JSON Syntax Highlighting */
3365
+ .json-key { color: #9cdcfe; background: none; }
3366
+ .json-string { color: #ce9178; background: none; }
3367
+ .json-number { color: #b5cea8; background: none; }
3368
+ .json-boolean { color: #569cd6; background: none; }
3369
+ .json-null { color: #569cd6; background: none; }
3370
+ </style>
3371
+ </head>
3372
+ <body>
3373
+ <div class="container">
3374
+ <div class="status-bar">
3375
+ <span class="status-code ${statusClass}">${response.status} ${response.statusText}</span>
3376
+ <span class="meta">⏱ ${response.duration}ms</span>
3377
+ <span class="meta">📦 ${bodySize}</span>
3378
+ </div>
3379
+
3380
+ <div class="request-info">
3381
+ <span class="method">${method}</span> ${this._escapeHtml(url)}
3382
+ </div>
3383
+
3384
+ <div class="tabs">
3385
+ <button class="tab active" data-tab="body">Body</button>
3386
+ <button class="tab" data-tab="headers">Headers</button>
3387
+ <button class="tab" data-tab="cookies">Cookies</button>
3388
+ </div>
3389
+
3390
+ <div id="body" class="tab-content active">
3391
+ <div class="body-container">
3392
+ <button class="compare-btn" onclick="compareBody()">⇄ Compare</button>
3393
+ <button class="copy-btn" onclick="copyBody()">📋 Copy</button>
3394
+ <pre><code>${bodyContent}</code></pre>
3395
+ </div>
3396
+ </div>
3397
+
3398
+ <div id="headers" class="tab-content">
3399
+ ${headersHtml}
3400
+ </div>
3401
+
3402
+ <div id="cookies" class="tab-content">
3403
+ ${cookiesHtml}
3404
+ </div>
3405
+
3406
+ <div id="norn-toast" class="norn-toast"></div>
3407
+ </div>
3408
+
3409
+ <script>
3410
+ const vscode = acquireVsCodeApi();
3411
+ const tabs = document.querySelectorAll('.tab');
3412
+ const contents = document.querySelectorAll('.tab-content');
3413
+
3414
+ tabs.forEach(tab => {
3415
+ tab.addEventListener('click', () => {
3416
+ tabs.forEach(t => t.classList.remove('active'));
3417
+ contents.forEach(c => c.classList.remove('active'));
3418
+
3419
+ tab.classList.add('active');
3420
+ document.getElementById(tab.dataset.tab).classList.add('active');
3421
+ });
3422
+ });
3423
+
3424
+ const rawBody = decodeURIComponent("${encodeURIComponent(typeof response.body === 'object' ? JSON.stringify(response.body, null, 2) : String(response.body))}");
3425
+
3426
+ // Response comparison state
3427
+ let compareBaseline = null;
3428
+
3429
+ function showToast(message) {
3430
+ const toast = document.getElementById('norn-toast');
3431
+ toast.textContent = message;
3432
+ toast.classList.add('visible');
3433
+ setTimeout(() => toast.classList.remove('visible'), 3000);
3434
+ }
3435
+
3436
+ function copyBody() {
3437
+ navigator.clipboard.writeText(rawBody).then(() => {
3438
+ const btn = document.querySelector('.copy-btn');
3439
+ btn.textContent = '✓ Copied!';
3440
+ setTimeout(() => btn.textContent = '📋 Copy', 2000);
3441
+ });
3442
+ }
3443
+
3444
+ function compareBody() {
3445
+ const btn = document.querySelector('.compare-btn');
3446
+
3447
+ if (!compareBaseline) {
3448
+ // First click - set baseline
3449
+ compareBaseline = rawBody;
3450
+ btn.classList.add('comparing');
3451
+ showToast('Select another response to compare');
3452
+ } else {
3453
+ // Second click - send compare request
3454
+ vscode.postMessage({ type: 'setBaseline', body: compareBaseline });
3455
+ vscode.postMessage({ type: 'compare', body: rawBody });
3456
+ compareBaseline = null;
3457
+ btn.classList.remove('comparing');
3458
+ }
3459
+ }
3460
+
3461
+ // Listen for reset compare message from extension
3462
+ window.addEventListener('message', event => {
3463
+ const message = event.data;
3464
+ if (message.type === 'resetCompare') {
3465
+ compareBaseline = null;
3466
+ const btn = document.querySelector('.compare-btn');
3467
+ if (btn) btn.classList.remove('comparing');
3468
+ }
3469
+ });
3470
+ </script>
3471
+ </body>
3472
+ </html>`;
3473
+ }
3474
+ _getStatusClass(status) {
3475
+ if (status >= 200 && status < 300) {
3476
+ return 'status-success';
3477
+ }
3478
+ if (status >= 300 && status < 400) {
3479
+ return 'status-redirect';
3480
+ }
3481
+ if (status >= 400 && status < 500) {
3482
+ return 'status-client-error';
3483
+ }
3484
+ return 'status-server-error';
3485
+ }
3486
+ _formatBody(body) {
3487
+ if (typeof body === 'object') {
3488
+ return this._syntaxHighlightJson(JSON.stringify(body, null, 2));
3489
+ }
3490
+ const stringBody = String(body);
3491
+ // Check if it looks like HTML
3492
+ if (stringBody.trim().startsWith('<') && stringBody.includes('>')) {
3493
+ return this._escapeHtml(stringBody);
3494
+ }
3495
+ return this._escapeHtml(stringBody);
3496
+ }
3497
+ _syntaxHighlightJson(json) {
3498
+ return json
3499
+ .replace(/&/g, '&amp;')
3500
+ .replace(/</g, '&lt;')
3501
+ .replace(/>/g, '&gt;')
3502
+ .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, (match) => {
3503
+ let cls = 'json-string';
3504
+ if (match.endsWith(':')) {
3505
+ cls = 'json-key';
3506
+ }
3507
+ return `<span class="${cls}">${match}</span>`;
3508
+ })
3509
+ .replace(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>')
3510
+ .replace(/\bnull\b/g, '<span class="json-null">null</span>')
3511
+ .replace(/\b(-?\d+\.?\d*)\b/g, '<span class="json-number">$1</span>');
3512
+ }
3513
+ _formatHeaders(headers) {
3514
+ const rows = Object.entries(headers)
3515
+ .map(([key, value]) => `
3516
+ <tr>
3517
+ <td class="header-name">${this._escapeHtml(key)}</td>
3518
+ <td class="header-value">${this._escapeHtml(String(value))}</td>
3519
+ </tr>
3520
+ `).join('');
3521
+ return `
3522
+ <table class="headers-table">
3523
+ <thead>
3524
+ <tr>
3525
+ <th>Name</th>
3526
+ <th>Value</th>
3527
+ </tr>
3528
+ </thead>
3529
+ <tbody>
3530
+ ${rows}
3531
+ </tbody>
3532
+ </table>
3533
+ `;
3534
+ }
3535
+ _getBodySize(body) {
3536
+ const str = typeof body === 'object' ? JSON.stringify(body) : String(body);
3537
+ const bytes = new TextEncoder().encode(str).length;
3538
+ if (bytes < 1024) {
3539
+ return `${bytes} B`;
3540
+ }
3541
+ if (bytes < 1024 * 1024) {
3542
+ return `${(bytes / 1024).toFixed(1)} KB`;
3543
+ }
3544
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3545
+ }
3546
+ _formatCookies(cookies) {
3547
+ if (!cookies || cookies.length === 0) {
3548
+ return '<p class="no-cookies">No cookies stored in session</p>';
3549
+ }
3550
+ // Group cookies by domain
3551
+ const byDomain = {};
3552
+ for (const cookie of cookies) {
3553
+ const domain = cookie.domain || 'unknown';
3554
+ if (!byDomain[domain]) {
3555
+ byDomain[domain] = [];
3556
+ }
3557
+ byDomain[domain].push(cookie);
3558
+ }
3559
+ const sections = Object.entries(byDomain).map(([domain, domainCookies]) => {
3560
+ const rows = domainCookies.map(cookie => `
3561
+ <tr>
3562
+ <td class="cookie-name">${this._escapeHtml(cookie.name)}</td>
3563
+ <td class="cookie-value" title="${this._escapeHtml(cookie.value)}">${this._escapeHtml(cookie.value.length > 40 ? cookie.value.substring(0, 40) + '...' : cookie.value)}</td>
3564
+ <td>${this._escapeHtml(cookie.path)}</td>
3565
+ <td>${cookie.expires ? new Date(cookie.expires).toLocaleDateString() : 'Session'}</td>
3566
+ <td class="cookie-flags">${cookie.httpOnly ? '🔒' : ''}${cookie.secure ? '🔐' : ''}</td>
3567
+ </tr>
3568
+ `).join('');
3569
+ return `
3570
+ <div class="cookie-domain-section">
3571
+ <h4 class="cookie-domain-header">🌐 ${this._escapeHtml(domain)} <span class="cookie-count">(${domainCookies.length})</span></h4>
3572
+ <table class="headers-table cookies-table">
3573
+ <thead>
3574
+ <tr>
3575
+ <th>Name</th>
3576
+ <th>Value</th>
3577
+ <th>Path</th>
3578
+ <th>Expires</th>
3579
+ <th>Flags</th>
3580
+ </tr>
3581
+ </thead>
3582
+ <tbody>
3583
+ ${rows}
3584
+ </tbody>
3585
+ </table>
3586
+ </div>
3587
+ `;
3588
+ }).join('');
3589
+ return `
3590
+ <div class="cookies-container">
3591
+ <p class="cookie-summary">🍪 ${cookies.length} cookie${cookies.length !== 1 ? 's' : ''} stored across ${Object.keys(byDomain).length} domain${Object.keys(byDomain).length !== 1 ? 's' : ''}</p>
3592
+ ${sections}
3593
+ <p class="cookie-info">💡 Cookies persist across requests. Use <code>Norn: Clear Cookies</code> command to reset.</p>
3594
+ </div>
3595
+ `;
3596
+ }
3597
+ /**
3598
+ * Generate the HTML for the Contract View panel
3599
+ * Shows schema validation errors in a rich, navigable UI
3600
+ */
3601
+ _getContractViewHtml(data) {
3602
+ const errorCount = data.errors.length;
3603
+ const schemaName = path.basename(data.schemaPath);
3604
+ // Group errors by severity
3605
+ const errorErrors = data.errors.filter(e => e.severity === 'error');
3606
+ const warningErrors = data.errors.filter(e => e.severity === 'warning');
3607
+ const infoErrors = data.errors.filter(e => e.severity === 'info');
3608
+ // Build errorsByPath map for annotated JSON
3609
+ const errorsByPath = new Map();
3610
+ for (const err of data.errors) {
3611
+ const path = err.instancePath || '/';
3612
+ if (!errorsByPath.has(path)) {
3613
+ errorsByPath.set(path, []);
3614
+ }
3615
+ errorsByPath.get(path).push(err);
3616
+ }
3617
+ // Generate annotated JSON view (same as Contract Report)
3618
+ const annotatedJson = this._generateAnnotatedJson(data.responseBody, errorsByPath, data.schema);
3619
+ // Format schema for display (schema doesn't need error annotations)
3620
+ const schemaStr = data.schema
3621
+ ? JSON.stringify(data.schema, null, 2)
3622
+ : '(Schema not available)';
3623
+ // For raw copy functionality
3624
+ const responseBodyStr = typeof data.responseBody === 'string'
3625
+ ? data.responseBody
3626
+ : JSON.stringify(data.responseBody, null, 2);
3627
+ // Build violations list HTML
3628
+ const violationsHtml = data.errors.map((err, idx) => {
3629
+ const severityClass = `severity-${err.severity}`;
3630
+ const severityBadge = err.severity === 'error' ? '❌' : err.severity === 'warning' ? '⚠️' : 'ℹ️';
3631
+ const actualDisplay = err.actual !== undefined
3632
+ ? this._escapeHtml(typeof err.actual === 'object' ? JSON.stringify(err.actual) : String(err.actual))
3633
+ : '(missing)';
3634
+ const expectedDisplay = err.expected ? this._escapeHtml(err.expected) : '';
3635
+ return `
3636
+ <div class="violation-item ${severityClass}" data-index="${idx}">
3637
+ <div class="violation-header">
3638
+ <span class="severity-badge">${severityBadge}</span>
3639
+ <span class="violation-path">${this._escapeHtml(err.instancePath)}</span>
3640
+ <span class="violation-keyword">${this._escapeHtml(err.keyword)}</span>
3641
+ </div>
3642
+ <div class="violation-message">${this._escapeHtml(err.message)}</div>
3643
+ <div class="violation-details">
3644
+ ${expectedDisplay ? `<div class="violation-expected"><strong>Expected:</strong> ${expectedDisplay}</div>` : ''}
3645
+ <div class="violation-actual"><strong>Actual:</strong> <code>${actualDisplay}</code></div>
3646
+ </div>
3647
+ <div class="violation-actions">
3648
+ <button class="action-btn" onclick="jumpToResponse('${this._escapeHtml(err.instancePath)}')">Jump to Response</button>
3649
+ <button class="action-btn" onclick="jumpToSchema('${this._escapeHtml(err.schemaPath)}')">Jump to Schema</button>
3650
+ </div>
3651
+ </div>
3652
+ `;
3653
+ }).join('');
3654
+ return /*html*/ `
3655
+ <!DOCTYPE html>
3656
+ <html lang="en">
3657
+ <head>
3658
+ <meta charset="UTF-8">
3659
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3660
+ <title>Contract View</title>
3661
+ <style>
3662
+ :root {
3663
+ --vscode-font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
3664
+ }
3665
+
3666
+ body {
3667
+ font-family: var(--vscode-font-family);
3668
+ padding: 16px;
3669
+ color: var(--vscode-foreground);
3670
+ background-color: var(--vscode-editor-background);
3671
+ line-height: 1.5;
3672
+ }
3673
+
3674
+ .contract-header {
3675
+ display: flex;
3676
+ align-items: center;
3677
+ gap: 12px;
3678
+ margin-bottom: 16px;
3679
+ padding-bottom: 12px;
3680
+ border-bottom: 1px solid var(--vscode-panel-border);
3681
+ }
3682
+
3683
+ .contract-icon {
3684
+ font-size: 24px;
3685
+ }
3686
+
3687
+ .contract-title {
3688
+ flex: 1;
3689
+ }
3690
+
3691
+ .contract-title h1 {
3692
+ margin: 0;
3693
+ font-size: 18px;
3694
+ font-weight: 600;
3695
+ }
3696
+
3697
+ .contract-title .schema-path {
3698
+ font-size: 12px;
3699
+ color: var(--vscode-descriptionForeground);
3700
+ margin-top: 4px;
3701
+ }
3702
+
3703
+ .summary {
3704
+ display: flex;
3705
+ gap: 16px;
3706
+ margin-bottom: 16px;
3707
+ }
3708
+
3709
+ .summary-item {
3710
+ padding: 8px 12px;
3711
+ border-radius: 4px;
3712
+ font-size: 13px;
3713
+ font-weight: 500;
3714
+ }
3715
+
3716
+ .summary-errors {
3717
+ background-color: rgba(244, 67, 54, 0.15);
3718
+ color: #f44336;
3719
+ }
3720
+
3721
+ .summary-warnings {
3722
+ background-color: rgba(255, 193, 7, 0.15);
3723
+ color: #ffc107;
3724
+ }
3725
+
3726
+ .summary-info {
3727
+ background-color: rgba(33, 150, 243, 0.15);
3728
+ color: #2196f3;
3729
+ }
3730
+
3731
+ .tabs {
3732
+ display: flex;
3733
+ border-bottom: 1px solid var(--vscode-panel-border);
3734
+ margin-bottom: 16px;
3735
+ }
3736
+
3737
+ .tab {
3738
+ padding: 8px 16px;
3739
+ cursor: pointer;
3740
+ border-bottom: 2px solid transparent;
3741
+ color: var(--vscode-foreground);
3742
+ opacity: 0.7;
3743
+ transition: opacity 0.2s, border-color 0.2s;
3744
+ }
3745
+
3746
+ .tab:hover {
3747
+ opacity: 1;
3748
+ }
3749
+
3750
+ .tab.active {
3751
+ opacity: 1;
3752
+ border-bottom-color: var(--vscode-focusBorder);
3753
+ }
3754
+
3755
+ .tab-content {
3756
+ display: none;
3757
+ }
3758
+
3759
+ .tab-content.active {
3760
+ display: block;
3761
+ }
3762
+
3763
+ .violation-item {
3764
+ margin-bottom: 12px;
3765
+ padding: 12px;
3766
+ border-radius: 6px;
3767
+ border-left: 4px solid;
3768
+ }
3769
+
3770
+ .violation-item.severity-error {
3771
+ background-color: rgba(244, 67, 54, 0.08);
3772
+ border-left-color: #f44336;
3773
+ }
3774
+
3775
+ .violation-item.severity-warning {
3776
+ background-color: rgba(255, 193, 7, 0.08);
3777
+ border-left-color: #ffc107;
3778
+ }
3779
+
3780
+ .violation-item.severity-info {
3781
+ background-color: rgba(33, 150, 243, 0.08);
3782
+ border-left-color: #2196f3;
3783
+ }
3784
+
3785
+ .violation-header {
3786
+ display: flex;
3787
+ align-items: center;
3788
+ gap: 8px;
3789
+ margin-bottom: 8px;
3790
+ }
3791
+
3792
+ .severity-badge {
3793
+ font-size: 14px;
3794
+ }
3795
+
3796
+ .violation-path {
3797
+ font-family: var(--vscode-editor-font-family);
3798
+ font-weight: 600;
3799
+ color: var(--vscode-textLink-foreground);
3800
+ }
3801
+
3802
+ .violation-keyword {
3803
+ font-size: 11px;
3804
+ padding: 2px 6px;
3805
+ border-radius: 3px;
3806
+ background-color: var(--vscode-badge-background);
3807
+ color: var(--vscode-badge-foreground);
3808
+ }
3809
+
3810
+ .violation-message {
3811
+ margin-bottom: 8px;
3812
+ }
3813
+
3814
+ .violation-details {
3815
+ font-size: 12px;
3816
+ color: var(--vscode-descriptionForeground);
3817
+ margin-bottom: 8px;
3818
+ }
3819
+
3820
+ .violation-details code {
3821
+ background-color: var(--vscode-textCodeBlock-background);
3822
+ padding: 2px 4px;
3823
+ border-radius: 3px;
3824
+ }
3825
+
3826
+ .violation-actions {
3827
+ display: flex;
3828
+ gap: 8px;
3829
+ }
3830
+
3831
+ .action-btn {
3832
+ padding: 4px 8px;
3833
+ font-size: 11px;
3834
+ border: 1px solid var(--vscode-button-secondaryBorder, var(--vscode-button-border));
3835
+ background-color: var(--vscode-button-secondaryBackground);
3836
+ color: var(--vscode-button-secondaryForeground);
3837
+ border-radius: 4px;
3838
+ cursor: pointer;
3839
+ }
3840
+
3841
+ .action-btn:hover {
3842
+ background-color: var(--vscode-button-secondaryHoverBackground);
3843
+ }
3844
+
3845
+ .code-container {
3846
+ border: 1px solid var(--vscode-panel-border);
3847
+ border-radius: 4px;
3848
+ background: var(--vscode-editor-background);
3849
+ }
3850
+
3851
+ .code-header {
3852
+ display: flex;
3853
+ justify-content: space-between;
3854
+ align-items: center;
3855
+ padding: 8px 12px;
3856
+ border-bottom: 1px solid var(--vscode-panel-border);
3857
+ background: var(--vscode-sideBar-background);
3858
+ }
3859
+
3860
+ .code-label {
3861
+ font-size: 12px;
3862
+ font-weight: 500;
3863
+ }
3864
+
3865
+ .copy-btn {
3866
+ padding: 4px 8px;
3867
+ font-size: 11px;
3868
+ border: none;
3869
+ background-color: transparent;
3870
+ color: var(--vscode-foreground);
3871
+ cursor: pointer;
3872
+ opacity: 0.7;
3873
+ }
3874
+
3875
+ .copy-btn:hover {
3876
+ opacity: 1;
3877
+ }
3878
+
3879
+ pre {
3880
+ margin: 0;
3881
+ padding: 12px;
3882
+ overflow: auto;
3883
+ max-height: 400px;
3884
+ font-size: 12px;
3885
+ line-height: 1.4;
3886
+ background: transparent !important;
3887
+ }
3888
+
3889
+ pre code {
3890
+ font-family: var(--vscode-editor-font-family);
3891
+ background: transparent !important;
3892
+ }
3893
+
3894
+ .json-formatted {
3895
+ background: transparent !important;
3896
+ } /* JSON syntax highlighting - explicit colors with !important for reliability */
3897
+ .json-key { color: #9cdcfe !important; }
3898
+ .json-string { color: #ce9178 !important; }
3899
+ .json-number { color: #b5cea8 !important; }
3900
+ .json-boolean { color: #569cd6 !important; }
3901
+ .json-null { color: #569cd6 !important; }
3902
+ .json-punctuation { color: var(--vscode-foreground); }
3903
+
3904
+ /* Annotated JSON styles (same as Contract Report) */
3905
+ .annotated-json {
3906
+ font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
3907
+ font-size: 13px;
3908
+ line-height: 1.5;
3909
+ background: var(--vscode-editor-background);
3910
+ border: 1px solid var(--vscode-panel-border);
3911
+ border-radius: 4px;
3912
+ padding: 12px;
3913
+ overflow-x: auto;
3914
+ white-space: pre;
3915
+ }
3916
+
3917
+ .json-line {
3918
+ display: block;
3919
+ white-space: pre;
3920
+ }
3921
+
3922
+ /* Property status highlights */
3923
+ .prop-status {
3924
+ border-radius: 3px;
3925
+ padding: 1px 3px;
3926
+ margin: -1px -3px;
3927
+ }
3928
+
3929
+ .prop-pass {
3930
+ /* No highlight for passing properties */
3931
+ }
3932
+
3933
+ .prop-fail {
3934
+ background: rgba(244, 67, 54, 0.25);
3935
+ box-shadow: 0 0 0 1px rgba(244, 67, 54, 0.6);
3936
+ }
3937
+
3938
+ .prop-warn {
3939
+ background: rgba(255, 193, 7, 0.25);
3940
+ box-shadow: 0 0 0 1px rgba(255, 193, 7, 0.6);
3941
+ }
3942
+
3943
+ /* JSON formatted in pre code blocks (for schema) */
3944
+ .json-formatted {
3945
+ font-family: var(--vscode-editor-font-family);
3946
+ }
3947
+
3948
+ .highlight-path {
3949
+ background-color: rgba(255, 235, 59, 0.3);
3950
+ border-radius: 2px;
3951
+ }
3952
+
3953
+ .empty-state {
3954
+ text-align: center;
3955
+ padding: 40px;
3956
+ color: var(--vscode-descriptionForeground);
3957
+ }
3958
+
3959
+ .empty-state-icon {
3960
+ font-size: 48px;
3961
+ margin-bottom: 16px;
3962
+ }
3963
+ </style>
3964
+ </head>
3965
+ <body>
3966
+ <div class="contract-header">
3967
+ <span class="contract-icon">📋</span>
3968
+ <div class="contract-title">
3969
+ <h1>Contract Validation</h1>
3970
+ <div class="schema-path">${this._escapeHtml(data.schemaPath)}</div>
3971
+ </div>
3972
+ </div>
3973
+
3974
+ <div class="summary">
3975
+ ${errorErrors.length > 0 ? `<div class="summary-item summary-errors">${errorErrors.length} error${errorErrors.length !== 1 ? 's' : ''}</div>` : ''}
3976
+ ${warningErrors.length > 0 ? `<div class="summary-item summary-warnings">${warningErrors.length} warning${warningErrors.length !== 1 ? 's' : ''}</div>` : ''}
3977
+ ${infoErrors.length > 0 ? `<div class="summary-item summary-info">${infoErrors.length} info</div>` : ''}
3978
+ </div>
3979
+
3980
+ <div class="tabs">
3981
+ <div class="tab active" onclick="switchTab('violations')">Violations (${errorCount})</div>
3982
+ <div class="tab" onclick="switchTab('response')">Response</div>
3983
+ <div class="tab" onclick="switchTab('schema')">Schema</div>
3984
+ </div>
3985
+
3986
+ <div id="tab-violations" class="tab-content active">
3987
+ ${errorCount > 0 ? violationsHtml : `
3988
+ <div class="empty-state">
3989
+ <div class="empty-state-icon">✅</div>
3990
+ <p>No violations found</p>
3991
+ </div>
3992
+ `}
3993
+ </div>
3994
+
3995
+ <div id="tab-response" class="tab-content">
3996
+ <div class="code-container">
3997
+ <div class="code-header">
3998
+ <span class="code-label">Response Body</span>
3999
+ <button class="copy-btn" onclick="copyCode('response')">📋 Copy</button>
4000
+ </div>
4001
+ <div class="annotated-json" id="response-code">${annotatedJson}</div>
4002
+ </div>
4003
+ <textarea id="response-raw" style="display:none;">${this._escapeHtml(responseBodyStr)}</textarea>
4004
+ </div>
4005
+
4006
+ <div id="tab-schema" class="tab-content">
4007
+ <div class="code-container">
4008
+ <div class="code-header">
4009
+ <span class="code-label">JSON Schema</span>
4010
+ <button class="copy-btn" onclick="copyCode('schema')">📋 Copy</button>
4011
+ </div>
4012
+ <pre><code id="schema-code" class="json-formatted">${this._formatJsonHtml(schemaStr)}</code></pre>
4013
+ </div>
4014
+ <textarea id="schema-raw" style="display:none;">${this._escapeHtml(schemaStr)}</textarea>
4015
+ </div>
4016
+
4017
+ <script>
4018
+ function switchTab(tabName) {
4019
+ // Update tab buttons
4020
+ document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
4021
+ event.target.classList.add('active');
4022
+
4023
+ // Update tab content
4024
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
4025
+ document.getElementById('tab-' + tabName).classList.add('active');
4026
+ }
4027
+
4028
+ function copyCode(type) {
4029
+ const textarea = document.getElementById(type + '-raw');
4030
+ const text = textarea.value || textarea.textContent || '';
4031
+ navigator.clipboard.writeText(text).then(() => {
4032
+ const btn = event.target;
4033
+ const originalText = btn.textContent;
4034
+ btn.textContent = '✓ Copied!';
4035
+ setTimeout(() => btn.textContent = originalText, 1500);
4036
+ });
4037
+ }
4038
+
4039
+ function jumpToResponse(jsonPath) {
4040
+ switchTab('response');
4041
+ // Highlight the path in the response (basic implementation)
4042
+ const codeEl = document.getElementById('response-code');
4043
+ // For now, just switch to the tab - advanced highlighting could be added later
4044
+ codeEl.scrollIntoView({ behavior: 'smooth' });
4045
+ }
4046
+
4047
+ function jumpToSchema(schemaPath) {
4048
+ switchTab('schema');
4049
+ const codeEl = document.getElementById('schema-code');
4050
+ codeEl.scrollIntoView({ behavior: 'smooth' });
4051
+ }
4052
+ </script>
4053
+ </body>
4054
+ </html>`;
4055
+ }
4056
+ /**
4057
+ * Generate HTML for the Contract Report view
4058
+ * Shows response body with inline status indicators for each property
4059
+ */
4060
+ _getContractReportHtml(data) {
4061
+ const errorCount = data.errors.length;
4062
+ const schemaName = path.basename(data.schemaPath);
4063
+ // Build a map of paths to errors for quick lookup
4064
+ const errorsByPath = new Map();
4065
+ for (const err of data.errors) {
4066
+ const path = err.instancePath || '/';
4067
+ if (!errorsByPath.has(path)) {
4068
+ errorsByPath.set(path, []);
4069
+ }
4070
+ errorsByPath.get(path).push(err);
4071
+ }
4072
+ // Get list of all paths that have been validated (from schema properties)
4073
+ const schemaPaths = this._extractSchemaPaths(data.schema);
4074
+ const matchedCount = schemaPaths.length - errorCount;
4075
+ // Format response body for display
4076
+ const responseBodyStr = typeof data.responseBody === 'string'
4077
+ ? data.responseBody
4078
+ : JSON.stringify(data.responseBody, null, 2);
4079
+ // Format schema for display
4080
+ const schemaStr = data.schema
4081
+ ? JSON.stringify(data.schema, null, 2)
4082
+ : '(Schema not available)';
4083
+ // Generate annotated JSON view
4084
+ const annotatedJson = this._generateAnnotatedJson(data.responseBody, errorsByPath, data.schema);
4085
+ // Group errors by severity for summary
4086
+ const breakingErrors = data.errors.filter(e => e.keyword === 'required' ||
4087
+ e.keyword === 'type' ||
4088
+ e.keyword === 'additionalProperties');
4089
+ const warningErrors = data.errors.filter(e => e.keyword !== 'required' &&
4090
+ e.keyword !== 'type' &&
4091
+ e.keyword !== 'additionalProperties');
4092
+ return /*html*/ `
4093
+ <!DOCTYPE html>
4094
+ <html lang="en">
4095
+ <head>
4096
+ <meta charset="UTF-8">
4097
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4098
+ <title>Contract Report</title>
4099
+ <style>
4100
+ :root {
4101
+ --vscode-font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
4102
+ }
4103
+
4104
+ body {
4105
+ font-family: var(--vscode-font-family);
4106
+ padding: 16px;
4107
+ color: var(--vscode-foreground);
4108
+ background-color: var(--vscode-editor-background);
4109
+ line-height: 1.5;
4110
+ }
4111
+
4112
+ .report-header {
4113
+ display: flex;
4114
+ align-items: center;
4115
+ gap: 16px;
4116
+ margin-bottom: 20px;
4117
+ padding-bottom: 16px;
4118
+ border-bottom: 1px solid var(--vscode-panel-border);
4119
+ }
4120
+
4121
+ .status-icon {
4122
+ font-size: 48px;
4123
+ }
4124
+
4125
+ .status-icon.pass { color: #4caf50; }
4126
+ .status-icon.fail { color: #f44336; }
4127
+
4128
+ .report-info h1 {
4129
+ margin: 0;
4130
+ font-size: 20px;
4131
+ font-weight: 600;
4132
+ }
4133
+
4134
+ .report-info .schema-path {
4135
+ font-size: 12px;
4136
+ color: var(--vscode-descriptionForeground);
4137
+ margin-top: 4px;
4138
+ }
4139
+
4140
+ .summary-cards {
4141
+ display: flex;
4142
+ gap: 12px;
4143
+ margin-bottom: 20px;
4144
+ }
4145
+
4146
+ .summary-card {
4147
+ padding: 12px 16px;
4148
+ border-radius: 6px;
4149
+ flex: 1;
4150
+ text-align: center;
4151
+ }
4152
+
4153
+ .summary-card.matched {
4154
+ background-color: rgba(76, 175, 80, 0.15);
4155
+ border: 1px solid rgba(76, 175, 80, 0.3);
4156
+ }
4157
+
4158
+ .summary-card.breaking {
4159
+ background-color: rgba(244, 67, 54, 0.15);
4160
+ border: 1px solid rgba(244, 67, 54, 0.3);
4161
+ }
4162
+
4163
+ .summary-card.warnings {
4164
+ background-color: rgba(255, 193, 7, 0.15);
4165
+ border: 1px solid rgba(255, 193, 7, 0.3);
4166
+ }
4167
+
4168
+ .summary-card .number {
4169
+ font-size: 28px;
4170
+ font-weight: bold;
4171
+ }
4172
+
4173
+ .summary-card.matched .number { color: #4caf50; }
4174
+ .summary-card.breaking .number { color: #f44336; }
4175
+ .summary-card.warnings .number { color: #ffc107; }
4176
+
4177
+ .summary-card .label {
4178
+ font-size: 12px;
4179
+ color: var(--vscode-descriptionForeground);
4180
+ margin-top: 4px;
4181
+ }
4182
+
4183
+ .tabs {
4184
+ display: flex;
4185
+ border-bottom: 1px solid var(--vscode-panel-border);
4186
+ margin-bottom: 16px;
4187
+ }
4188
+
4189
+ .tab {
4190
+ padding: 8px 16px;
4191
+ cursor: pointer;
4192
+ border-bottom: 2px solid transparent;
4193
+ color: var(--vscode-foreground);
4194
+ opacity: 0.7;
4195
+ transition: opacity 0.2s, border-color 0.2s;
4196
+ }
4197
+
4198
+ .tab:hover { opacity: 1; }
4199
+ .tab.active {
4200
+ opacity: 1;
4201
+ border-bottom-color: var(--vscode-focusBorder);
4202
+ }
4203
+
4204
+ .tab-content { display: none; }
4205
+ .tab-content.active { display: block; }
4206
+
4207
+ /* Annotated JSON styles */
4208
+ .annotated-json {
4209
+ font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
4210
+ font-size: 13px;
4211
+ line-height: 1.5;
4212
+ background: var(--vscode-editor-background);
4213
+ border: 1px solid var(--vscode-panel-border);
4214
+ border-radius: 4px;
4215
+ padding: 12px;
4216
+ overflow-x: auto;
4217
+ white-space: pre;
4218
+ }
4219
+
4220
+ .json-line {
4221
+ display: block;
4222
+ white-space: pre;
4223
+ }
4224
+
4225
+ /* Property status highlights - similar to assert hover */
4226
+ .prop-status {
4227
+ border-radius: 3px;
4228
+ padding: 1px 3px;
4229
+ margin: -1px -3px;
4230
+ }
4231
+
4232
+ .prop-pass {
4233
+ /* No highlight for passing properties - clean look */
4234
+ }
4235
+
4236
+ .prop-fail {
4237
+ background: rgba(244, 67, 54, 0.25);
4238
+ box-shadow: 0 0 0 1px rgba(244, 67, 54, 0.6);
4239
+ }
4240
+
4241
+ .prop-warn {
4242
+ background: rgba(255, 193, 7, 0.25);
4243
+ box-shadow: 0 0 0 1px rgba(255, 193, 7, 0.6);
4244
+ }
4245
+
4246
+ .prop-error {
4247
+ background: rgba(244, 67, 54, 0.15);
4248
+ border-radius: 3px;
4249
+ padding: 0 4px;
4250
+ }
4251
+
4252
+ .prop-warning {
4253
+ background: rgba(255, 193, 7, 0.15);
4254
+ border-radius: 3px;
4255
+ padding: 0 4px;
4256
+ }
4257
+
4258
+ .error-tooltip {
4259
+ display: none;
4260
+ position: absolute;
4261
+ background: var(--vscode-editorWidget-background);
4262
+ border: 1px solid var(--vscode-editorWidget-border);
4263
+ padding: 8px 12px;
4264
+ border-radius: 4px;
4265
+ font-size: 12px;
4266
+ max-width: 400px;
4267
+ z-index: 100;
4268
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
4269
+ }
4270
+
4271
+ .json-line:hover .error-tooltip {
4272
+ display: block;
4273
+ }
4274
+
4275
+ /* Issues list styles */
4276
+ .issue-item {
4277
+ margin-bottom: 12px;
4278
+ padding: 12px;
4279
+ border-radius: 6px;
4280
+ border-left: 4px solid;
4281
+ }
4282
+
4283
+ .issue-item.breaking {
4284
+ background-color: rgba(244, 67, 54, 0.08);
4285
+ border-left-color: #f44336;
4286
+ }
4287
+
4288
+ .issue-item.warning {
4289
+ background-color: rgba(255, 193, 7, 0.08);
4290
+ border-left-color: #ffc107;
4291
+ }
4292
+
4293
+ .issue-header {
4294
+ display: flex;
4295
+ align-items: center;
4296
+ gap: 8px;
4297
+ font-weight: 500;
4298
+ }
4299
+
4300
+ .issue-path {
4301
+ font-family: var(--vscode-font-family);
4302
+ color: var(--vscode-textLink-foreground);
4303
+ }
4304
+
4305
+ .issue-message {
4306
+ margin-top: 4px;
4307
+ color: var(--vscode-descriptionForeground);
4308
+ }
4309
+
4310
+ .issue-details {
4311
+ margin-top: 8px;
4312
+ font-size: 12px;
4313
+ }
4314
+
4315
+ .issue-expected, .issue-actual {
4316
+ margin-top: 4px;
4317
+ }
4318
+
4319
+ /* JSON syntax highlighting - explicit colors for reliability */
4320
+ .json-key { color: #9cdcfe !important; }
4321
+ .json-string { color: #ce9178 !important; }
4322
+ .json-number { color: #b5cea8 !important; }
4323
+ .json-boolean { color: #569cd6 !important; }
4324
+ .json-null { color: #569cd6 !important; }
4325
+ .json-punctuation { color: var(--vscode-foreground); }
4326
+
4327
+ .code-container {
4328
+ border: 1px solid var(--vscode-panel-border);
4329
+ border-radius: 4px;
4330
+ background: var(--vscode-editor-background);
4331
+ }
4332
+
4333
+ .code-header {
4334
+ display: flex;
4335
+ justify-content: space-between;
4336
+ align-items: center;
4337
+ padding: 8px 12px;
4338
+ border-bottom: 1px solid var(--vscode-panel-border);
4339
+ background: var(--vscode-sideBar-background);
4340
+ }
4341
+
4342
+ .code-label {
4343
+ font-weight: 500;
4344
+ font-size: 12px;
4345
+ color: var(--vscode-descriptionForeground);
4346
+ }
4347
+
4348
+ .copy-btn {
4349
+ background: transparent;
4350
+ border: 1px solid var(--vscode-button-secondaryBackground);
4351
+ color: var(--vscode-foreground);
4352
+ padding: 4px 8px;
4353
+ border-radius: 3px;
4354
+ cursor: pointer;
4355
+ font-size: 11px;
4356
+ }
4357
+
4358
+ .copy-btn:hover {
4359
+ background: var(--vscode-button-secondaryHoverBackground);
4360
+ }
4361
+
4362
+ pre {
4363
+ margin: 0;
4364
+ padding: 12px;
4365
+ overflow-x: auto;
4366
+ background: transparent;
4367
+ white-space: pre;
4368
+ }
4369
+
4370
+ code {
4371
+ font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
4372
+ font-size: 13px;
4373
+ background: transparent;
4374
+ white-space: pre;
4375
+ line-height: 1.5;
4376
+ }
4377
+
4378
+ /* Formatted JSON in schema tab */
4379
+ code.json-formatted {
4380
+ display: block;
4381
+ white-space: pre;
4382
+ line-height: 1.5;
4383
+ background: transparent;
4384
+ }
4385
+
4386
+ /* Ensure spans don't have background */
4387
+ code span {
4388
+ background: transparent;
4389
+ }
4390
+ </style>
4391
+ </head>
4392
+ <body>
4393
+ <!-- Header -->
4394
+ <div class="report-header">
4395
+ <div class="status-icon ${errorCount === 0 ? 'pass' : 'fail'}">
4396
+ ${errorCount === 0 ? '✓' : '⚠'}
4397
+ </div>
4398
+ <div class="report-info">
4399
+ <h1>${errorCount === 0 ? 'Contract Validation Passed' : `${errorCount} Issue${errorCount !== 1 ? 's' : ''} Found`}</h1>
4400
+ <div class="schema-path">${this._escapeHtml(data.schemaPath)}</div>
4401
+ </div>
4402
+ </div>
4403
+
4404
+ <!-- Summary Cards -->
4405
+ <div class="summary-cards">
4406
+ <div class="summary-card matched">
4407
+ <div class="number">${matchedCount >= 0 ? matchedCount : '?'}</div>
4408
+ <div class="label">Matched</div>
4409
+ </div>
4410
+ <div class="summary-card breaking">
4411
+ <div class="number">${breakingErrors.length}</div>
4412
+ <div class="label">Breaking</div>
4413
+ </div>
4414
+ <div class="summary-card warnings">
4415
+ <div class="number">${warningErrors.length}</div>
4416
+ <div class="label">Warnings</div>
4417
+ </div>
4418
+ </div>
4419
+
4420
+ <!-- Tabs -->
4421
+ <div class="tabs">
4422
+ <div class="tab active" onclick="switchTab('annotated')">Annotated Response</div>
4423
+ <div class="tab" onclick="switchTab('issues')">Issues (${errorCount})</div>
4424
+ <div class="tab" onclick="switchTab('schema')">Schema</div>
4425
+ </div>
4426
+
4427
+ <!-- Annotated Response Tab -->
4428
+ <div id="tab-annotated" class="tab-content active">
4429
+ <div class="annotated-json">
4430
+ ${annotatedJson}
4431
+ </div>
4432
+ </div>
4433
+
4434
+ <!-- Issues Tab -->
4435
+ <div id="tab-issues" class="tab-content">
4436
+ ${errorCount === 0 ? '<p style="color: var(--vscode-descriptionForeground);">No issues found - all properties match the schema!</p>' : ''}
4437
+
4438
+ ${breakingErrors.length > 0 ? `
4439
+ <h3 style="color: #f44336; margin-top: 0;">Breaking Changes</h3>
4440
+ ${breakingErrors.map(err => `
4441
+ <div class="issue-item breaking">
4442
+ <div class="issue-header">
4443
+ <span>❌</span>
4444
+ <span class="issue-path">${this._escapeHtml(err.instancePath || '/')}</span>
4445
+ <span style="color: var(--vscode-descriptionForeground);">(${err.keyword})</span>
4446
+ </div>
4447
+ <div class="issue-message">${this._escapeHtml(err.message)}</div>
4448
+ <div class="issue-details">
4449
+ ${err.expected ? `<div class="issue-expected"><strong>Expected:</strong> ${this._escapeHtml(err.expected)}</div>` : ''}
4450
+ ${err.actual !== undefined ? `<div class="issue-actual"><strong>Actual:</strong> <code>${this._escapeHtml(typeof err.actual === 'object' ? JSON.stringify(err.actual) : String(err.actual))}</code></div>` : ''}
4451
+ </div>
4452
+ </div>
4453
+ `).join('')}
4454
+ ` : ''}
4455
+
4456
+ ${warningErrors.length > 0 ? `
4457
+ <h3 style="color: #ffc107; margin-top: ${breakingErrors.length > 0 ? '24px' : '0'};">Warnings</h3>
4458
+ ${warningErrors.map(err => `
4459
+ <div class="issue-item warning">
4460
+ <div class="issue-header">
4461
+ <span>⚠️</span>
4462
+ <span class="issue-path">${this._escapeHtml(err.instancePath || '/')}</span>
4463
+ <span style="color: var(--vscode-descriptionForeground);">(${err.keyword})</span>
4464
+ </div>
4465
+ <div class="issue-message">${this._escapeHtml(err.message)}</div>
4466
+ <div class="issue-details">
4467
+ ${err.expected ? `<div class="issue-expected"><strong>Expected:</strong> ${this._escapeHtml(err.expected)}</div>` : ''}
4468
+ ${err.actual !== undefined ? `<div class="issue-actual"><strong>Actual:</strong> <code>${this._escapeHtml(typeof err.actual === 'object' ? JSON.stringify(err.actual) : String(err.actual))}</code></div>` : ''}
4469
+ </div>
4470
+ </div>
4471
+ `).join('')}
4472
+ ` : ''}
4473
+ </div>
4474
+
4475
+ <!-- Schema Tab -->
4476
+ <div id="tab-schema" class="tab-content">
4477
+ <div class="code-container">
4478
+ <div class="code-header">
4479
+ <span class="code-label">JSON Schema</span>
4480
+ <button class="copy-btn" onclick="copySchema()">📋 Copy</button>
4481
+ </div>
4482
+ <pre><code class="json-formatted">${this._formatJsonHtml(schemaStr)}</code></pre>
4483
+ </div>
4484
+ <textarea id="schema-raw" style="display:none;">${this._escapeHtml(schemaStr)}</textarea>
4485
+ </div>
4486
+
4487
+ <script>
4488
+ function switchTab(tabName) {
4489
+ document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
4490
+ event.target.classList.add('active');
4491
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
4492
+ document.getElementById('tab-' + tabName).classList.add('active');
4493
+ }
4494
+
4495
+ function copySchema() {
4496
+ const text = document.getElementById('schema-raw').value;
4497
+ navigator.clipboard.writeText(text).then(() => {
4498
+ const btn = event.target;
4499
+ const originalText = btn.textContent;
4500
+ btn.textContent = '✓ Copied!';
4501
+ setTimeout(() => btn.textContent = originalText, 1500);
4502
+ });
4503
+ }
4504
+ </script>
4505
+ </body>
4506
+ </html>`;
4507
+ }
4508
+ /**
4509
+ * Extract all property paths from a JSON Schema
4510
+ */
4511
+ _extractSchemaPaths(schema, prefix = '') {
4512
+ if (!schema || typeof schema !== 'object')
4513
+ return [];
4514
+ const paths = [];
4515
+ if (schema.properties) {
4516
+ for (const [key, prop] of Object.entries(schema.properties)) {
4517
+ const path = prefix ? `${prefix}/${key}` : `/${key}`;
4518
+ paths.push(path);
4519
+ paths.push(...this._extractSchemaPaths(prop, path));
4520
+ }
4521
+ }
4522
+ if (schema.items) {
4523
+ paths.push(...this._extractSchemaPaths(schema.items, `${prefix}/0`));
4524
+ }
4525
+ return paths;
4526
+ }
4527
+ /**
4528
+ * Generate annotated JSON HTML with status indicators per line
4529
+ */
4530
+ _generateAnnotatedJson(data, errorsByPath, schema) {
4531
+ if (!data || typeof data !== 'object') {
4532
+ const hasError = errorsByPath.has('/') || errorsByPath.has('');
4533
+ const lineClass = hasError ? 'line-fail' : 'line-pass';
4534
+ return `<div class="json-line ${lineClass}">${this._escapeHtml(JSON.stringify(data))}</div>`;
4535
+ }
4536
+ const lines = [];
4537
+ this._annotateJsonRecursive(data, errorsByPath, '', 0, lines);
4538
+ return lines.join('');
4539
+ }
4540
+ /**
4541
+ * Recursively annotate JSON object with status indicators
4542
+ */
4543
+ _annotateJsonRecursive(data, errorsByPath, currentPath, indent, lines) {
4544
+ const indentStr = ' '.repeat(indent);
4545
+ if (Array.isArray(data)) {
4546
+ lines.push(this._createJsonLine(currentPath, errorsByPath, `${indentStr}<span class="json-punctuation">[</span>`, true, ''));
4547
+ for (let i = 0; i < data.length; i++) {
4548
+ const itemPath = `${currentPath}/${i}`;
4549
+ const item = data[i];
4550
+ const isLast = i === data.length - 1;
4551
+ if (typeof item === 'object' && item !== null) {
4552
+ this._annotateJsonRecursive(item, errorsByPath, itemPath, indent + 1, lines);
4553
+ // Add comma if not last
4554
+ if (!isLast) {
4555
+ const lastLine = lines[lines.length - 1];
4556
+ lines[lines.length - 1] = lastLine.replace('</span></div>', ',</span></div>');
4557
+ }
4558
+ }
4559
+ else {
4560
+ const valueStr = this._formatJsonValue(item);
4561
+ const itemIndent = ' '.repeat(indent + 1);
4562
+ lines.push(this._createJsonLine(itemPath, errorsByPath, `${valueStr}${isLast ? '' : ','}`, false, itemIndent));
4563
+ }
4564
+ }
4565
+ lines.push(this._createJsonLine(currentPath, errorsByPath, `${indentStr}<span class="json-punctuation">]</span>`, true, ''));
4566
+ }
4567
+ else if (typeof data === 'object' && data !== null) {
4568
+ lines.push(this._createJsonLine(currentPath, errorsByPath, `${indentStr}<span class="json-punctuation">{</span>`, true, ''));
4569
+ const keys = Object.keys(data);
4570
+ for (let i = 0; i < keys.length; i++) {
4571
+ const key = keys[i];
4572
+ const value = data[key];
4573
+ const propPath = `${currentPath}/${key}`;
4574
+ const isLast = i === keys.length - 1;
4575
+ const propIndent = ' '.repeat(indent + 1);
4576
+ if (typeof value === 'object' && value !== null) {
4577
+ // Nested object - key line is structural, no highlight
4578
+ lines.push(this._createJsonLine(propPath, errorsByPath, `${propIndent}<span class="json-key">"${this._escapeHtml(key)}"</span><span class="json-punctuation">:</span> `, true, ''));
4579
+ this._annotateJsonRecursive(value, errorsByPath, propPath, indent + 1, lines);
4580
+ if (!isLast) {
4581
+ const lastLine = lines[lines.length - 1];
4582
+ lines[lines.length - 1] = lastLine.replace('</span></div>', ',</span></div>');
4583
+ }
4584
+ }
4585
+ else {
4586
+ // Primitive value - highlight the key:value pair
4587
+ const valueStr = this._formatJsonValue(value);
4588
+ lines.push(this._createJsonLine(propPath, errorsByPath, `<span class="json-key">"${this._escapeHtml(key)}"</span><span class="json-punctuation">:</span> ${valueStr}${isLast ? '' : ','}`, false, propIndent));
4589
+ }
4590
+ }
4591
+ lines.push(this._createJsonLine(currentPath, errorsByPath, `${indentStr}<span class="json-punctuation">}</span>`, true, ''));
4592
+ }
4593
+ }
4594
+ /**
4595
+ * Create a single annotated JSON line with property highlight
4596
+ */
4597
+ _createJsonLine(path, errorsByPath, content, isBracket = false, indent = '') {
4598
+ // For brackets, just return the line without any highlight
4599
+ if (isBracket) {
4600
+ return `<div class="json-line">${content}</div>`;
4601
+ }
4602
+ const errors = errorsByPath.get(path);
4603
+ let propClass = 'prop-pass';
4604
+ let tooltip = '';
4605
+ if (errors && errors.length > 0) {
4606
+ const isBreaking = errors.some(e => e.keyword === 'required' ||
4607
+ e.keyword === 'type' ||
4608
+ e.keyword === 'additionalProperties');
4609
+ propClass = isBreaking ? 'prop-fail' : 'prop-warn';
4610
+ tooltip = errors.map((e) => `${e.keyword}: ${e.message}`).join('\n');
4611
+ }
4612
+ // Indent outside the highlighted span, property content inside
4613
+ return `<div class="json-line">${indent}<span class="prop-status ${propClass}" ${tooltip ? `title="${this._escapeHtml(tooltip)}"` : ''}>${content}</span></div>`;
4614
+ }
4615
+ /**
4616
+ * Format a primitive JSON value with syntax highlighting
4617
+ */
4618
+ _formatJsonValue(value) {
4619
+ if (value === null) {
4620
+ return '<span class="json-null">null</span>';
4621
+ }
4622
+ if (typeof value === 'boolean') {
4623
+ return `<span class="json-boolean">${value}</span>`;
4624
+ }
4625
+ if (typeof value === 'number') {
4626
+ return `<span class="json-number">${value}</span>`;
4627
+ }
4628
+ if (typeof value === 'string') {
4629
+ return `<span class="json-string">"${this._escapeHtml(value)}"</span>`;
4630
+ }
4631
+ return this._escapeHtml(String(value));
4632
+ }
4633
+ /**
4634
+ * Format JSON string with syntax highlighting HTML
4635
+ */
4636
+ _formatJsonHtml(json) {
4637
+ // Do syntax highlighting first, then escape only the values
4638
+ // This preserves the HTML tags we're adding
4639
+ return json
4640
+ // First, escape any < > & in the actual content
4641
+ .replace(/&/g, '&amp;')
4642
+ .replace(/</g, '&lt;')
4643
+ .replace(/>/g, '&gt;')
4644
+ // Now apply syntax highlighting (quotes are still intact)
4645
+ .replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
4646
+ .replace(/: "([^"]*)"/g, ': <span class="json-string">"$1"</span>')
4647
+ .replace(/: (-?\d+\.?\d*)\b/g, ': <span class="json-number">$1</span>')
4648
+ .replace(/: (true|false)\b/g, ': <span class="json-boolean">$1</span>')
4649
+ .replace(/: (null)\b/g, ': <span class="json-null">$1</span>')
4650
+ // Handle array values (not just after colon)
4651
+ .replace(/\[\s*"([^"]*)"/g, '[ <span class="json-string">"$1"</span>')
4652
+ .replace(/,\s*"([^"]*)"(?=\s*[,\]])/g, ', <span class="json-string">"$1"</span>');
4653
+ }
4654
+ _escapeHtml(text) {
4655
+ return text
4656
+ .replace(/&/g, '&amp;')
4657
+ .replace(/</g, '&lt;')
4658
+ .replace(/>/g, '&gt;')
4659
+ .replace(/"/g, '&quot;')
4660
+ .replace(/'/g, '&#039;');
4661
+ }
4662
+ dispose() {
4663
+ ResponsePanel.currentPanel = undefined;
4664
+ this._panel.dispose();
4665
+ while (this._disposables.length) {
4666
+ const disposable = this._disposables.pop();
4667
+ if (disposable) {
4668
+ disposable.dispose();
4669
+ }
4670
+ }
4671
+ }
4672
+ }
4673
+ exports.ResponsePanel = ResponsePanel;
4674
+ //# sourceMappingURL=responsePanel.js.map