mcp-feedback-enhanced 0.1.64 → 0.1.67
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.
- package/dist/index.js +571 -32
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,7 +16,9 @@ import * as os from 'os';
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import * as net from 'net';
|
|
19
|
+
import * as http from 'http';
|
|
19
20
|
import { createRequire } from 'module';
|
|
21
|
+
import { exec } from 'child_process';
|
|
20
22
|
// Read version from package.json (ES module compatible)
|
|
21
23
|
const require = createRequire(import.meta.url);
|
|
22
24
|
const packageJson = require('../package.json');
|
|
@@ -167,7 +169,7 @@ function getLiveServers() {
|
|
|
167
169
|
* Now async to support port connectivity checks
|
|
168
170
|
*/
|
|
169
171
|
async function findExtensionForProjectAsync(projectPath) {
|
|
170
|
-
|
|
172
|
+
let servers = await getLiveServersAsync();
|
|
171
173
|
debug(`Looking for Extension for project: ${projectPath}`);
|
|
172
174
|
debug(`Found ${servers.length} live Extension server(s) with valid ports`);
|
|
173
175
|
if (servers.length === 0) {
|
|
@@ -176,15 +178,40 @@ async function findExtensionForProjectAsync(projectPath) {
|
|
|
176
178
|
// Get MCP server's CURSOR_TRACE_ID (if running in Cursor)
|
|
177
179
|
const myTraceId = process.env['CURSOR_TRACE_ID'] || '';
|
|
178
180
|
debug(`My CURSOR_TRACE_ID: ${myTraceId || '(not set)'}`);
|
|
179
|
-
// Strategy 0: CURSOR_TRACE_ID match
|
|
181
|
+
// Strategy 0: CURSOR_TRACE_ID match - but only if UNIQUE match
|
|
182
|
+
// (Multiple windows can share the same trace ID, so we need workspace matching as tiebreaker)
|
|
180
183
|
if (myTraceId) {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
const traceMatches = servers.filter(s => s.cursorTraceId === myTraceId);
|
|
185
|
+
if (traceMatches.length === 1) {
|
|
186
|
+
// Unique trace ID match - perfect!
|
|
187
|
+
debug(`✓ CURSOR_TRACE_ID unique match: port=${traceMatches[0].port}`);
|
|
188
|
+
return traceMatches[0];
|
|
189
|
+
}
|
|
190
|
+
else if (traceMatches.length > 1) {
|
|
191
|
+
// Multiple servers with same trace ID - use workspace matching among them
|
|
192
|
+
debug(`Multiple servers (${traceMatches.length}) with same CURSOR_TRACE_ID, using workspace match`);
|
|
193
|
+
// Try exact workspace match among trace-matched servers
|
|
194
|
+
const exactMatch = traceMatches.find(s => s.workspaces?.includes(projectPath));
|
|
195
|
+
if (exactMatch) {
|
|
196
|
+
debug(`✓ CURSOR_TRACE_ID + workspace match: port=${exactMatch.port}`);
|
|
197
|
+
return exactMatch;
|
|
198
|
+
}
|
|
199
|
+
// Try prefix match among trace-matched servers
|
|
200
|
+
for (const server of traceMatches) {
|
|
201
|
+
for (const ws of server.workspaces || []) {
|
|
202
|
+
if (projectPath.startsWith(ws + path.sep) || ws.startsWith(projectPath + path.sep)) {
|
|
203
|
+
debug(`✓ CURSOR_TRACE_ID + prefix match: port=${server.port}`);
|
|
204
|
+
return server;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Fall back to most recent among trace-matched servers
|
|
209
|
+
const sorted = traceMatches.sort((a, b) => b.timestamp - a.timestamp);
|
|
210
|
+
debug(`✓ CURSOR_TRACE_ID + most recent: port=${sorted[0].port}`);
|
|
211
|
+
return sorted[0];
|
|
185
212
|
}
|
|
186
213
|
}
|
|
187
|
-
// Strategy 1: Exact workspace match
|
|
214
|
+
// Strategy 1: Exact workspace match (when no trace ID or no trace match)
|
|
188
215
|
const workspaceMatches = servers.filter(s => s.workspaces?.includes(projectPath));
|
|
189
216
|
if (workspaceMatches.length === 1) {
|
|
190
217
|
debug(`✓ Exact workspace match (single): port=${workspaceMatches[0].port}`);
|
|
@@ -233,14 +260,31 @@ function findExtensionForProject(projectPath) {
|
|
|
233
260
|
const servers = getLiveServers();
|
|
234
261
|
if (servers.length === 0)
|
|
235
262
|
return null;
|
|
236
|
-
// Strategy 0: CURSOR_TRACE_ID match
|
|
263
|
+
// Strategy 0: CURSOR_TRACE_ID match - but only if UNIQUE
|
|
237
264
|
const myTraceId = process.env['CURSOR_TRACE_ID'] || '';
|
|
238
265
|
if (myTraceId) {
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
return
|
|
266
|
+
const traceMatches = servers.filter(s => s.cursorTraceId === myTraceId);
|
|
267
|
+
if (traceMatches.length === 1) {
|
|
268
|
+
return traceMatches[0];
|
|
269
|
+
}
|
|
270
|
+
else if (traceMatches.length > 1) {
|
|
271
|
+
// Multiple servers with same trace ID - try workspace match among them
|
|
272
|
+
const exactMatch = traceMatches.find(s => s.workspaces?.includes(projectPath));
|
|
273
|
+
if (exactMatch)
|
|
274
|
+
return exactMatch;
|
|
275
|
+
// Prefix match among trace-matched
|
|
276
|
+
for (const server of traceMatches) {
|
|
277
|
+
for (const ws of server.workspaces || []) {
|
|
278
|
+
if (projectPath.startsWith(ws + path.sep) || ws.startsWith(projectPath + path.sep)) {
|
|
279
|
+
return server;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Most recent among trace-matched
|
|
284
|
+
return traceMatches.sort((a, b) => b.timestamp - a.timestamp)[0];
|
|
285
|
+
}
|
|
242
286
|
}
|
|
243
|
-
// Strategy 1: Exact workspace match
|
|
287
|
+
// Strategy 1: Exact workspace match (when no trace ID or no trace match)
|
|
244
288
|
const workspaceMatches = servers.filter(s => s.workspaces?.includes(projectPath));
|
|
245
289
|
if (workspaceMatches.length >= 1) {
|
|
246
290
|
return workspaceMatches.sort((a, b) => b.timestamp - a.timestamp)[0];
|
|
@@ -412,34 +456,529 @@ function handleMessage(message) {
|
|
|
412
456
|
break;
|
|
413
457
|
}
|
|
414
458
|
}
|
|
459
|
+
// Track tried servers to avoid retry loops
|
|
460
|
+
let triedServerPorts = new Set();
|
|
415
461
|
/**
|
|
416
462
|
* Request feedback from user via Extension
|
|
463
|
+
* Uses single-connection with fast retry strategy:
|
|
464
|
+
* - Sorts servers by relevance (workspace match, most recent)
|
|
465
|
+
* - Tries one server at a time
|
|
466
|
+
* - If rejected (PROJECT_NOT_IN_WINDOW or no webview), immediately tries next
|
|
467
|
+
* - Falls back to browser if all fail
|
|
417
468
|
*/
|
|
418
469
|
async function requestFeedback(projectDirectory, summary, timeout, agentName) {
|
|
419
|
-
|
|
470
|
+
// Reset tried servers
|
|
471
|
+
triedServerPorts.clear();
|
|
472
|
+
// Get all servers sorted by relevance
|
|
473
|
+
const servers = await getSortedServers(projectDirectory);
|
|
474
|
+
if (servers.length === 0) {
|
|
475
|
+
debug('No Extension servers found, falling back to browser');
|
|
476
|
+
return requestFeedbackViaBrowser(projectDirectory, summary, timeout);
|
|
477
|
+
}
|
|
478
|
+
debug(`Found ${servers.length} server(s), trying in order of relevance...`);
|
|
479
|
+
// Try servers one by one until success
|
|
480
|
+
return tryServersSequentially(servers, projectDirectory, summary, timeout, agentName);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Get servers sorted by relevance for the project
|
|
484
|
+
*/
|
|
485
|
+
async function getSortedServers(projectPath) {
|
|
486
|
+
const servers = await getLiveServersAsync();
|
|
487
|
+
if (servers.length === 0) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
// Calculate workspace frequency for uniqueness bonus
|
|
491
|
+
const workspaceFrequency = new Map();
|
|
492
|
+
servers.forEach(s => {
|
|
493
|
+
s.workspaces?.forEach(ws => {
|
|
494
|
+
workspaceFrequency.set(ws, (workspaceFrequency.get(ws) || 0) + 1);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
// Score each server
|
|
498
|
+
const scored = servers.map(server => {
|
|
499
|
+
let score = 0;
|
|
500
|
+
let reasons = [];
|
|
501
|
+
// 1. FOCUSED WINDOW (+500) - Highest priority!
|
|
502
|
+
if (server.focused) {
|
|
503
|
+
score += 500;
|
|
504
|
+
reasons.push('focused');
|
|
505
|
+
}
|
|
506
|
+
// 2. Workspace UNIQUENESS bonus (+1000)
|
|
507
|
+
// If this project is ONLY in this server's workspaces, huge bonus
|
|
508
|
+
if (server.workspaces?.includes(projectPath)) {
|
|
509
|
+
const freq = workspaceFrequency.get(projectPath) || 1;
|
|
510
|
+
if (freq === 1) {
|
|
511
|
+
score += 1000;
|
|
512
|
+
reasons.push('unique-ws');
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
score += 100;
|
|
516
|
+
reasons.push('shared-ws');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// 3. Prefix match (+50)
|
|
520
|
+
for (const ws of server.workspaces || []) {
|
|
521
|
+
if (projectPath.startsWith(ws + path.sep)) {
|
|
522
|
+
score += 50;
|
|
523
|
+
reasons.push('prefix');
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// 4. CURSOR_TRACE_ID match (+10) - Low priority since often shared
|
|
528
|
+
const myTraceId = process.env['CURSOR_TRACE_ID'] || '';
|
|
529
|
+
if (myTraceId && server.cursorTraceId === myTraceId) {
|
|
530
|
+
score += 10;
|
|
531
|
+
reasons.push('trace');
|
|
532
|
+
}
|
|
533
|
+
// 5. More recent timestamp (tiny bonus)
|
|
534
|
+
score += server.timestamp / (10 ** 14);
|
|
535
|
+
return { server, score, reasons };
|
|
536
|
+
});
|
|
537
|
+
// Sort by score descending
|
|
538
|
+
scored.sort((a, b) => b.score - a.score);
|
|
539
|
+
debug(`Server scores:`);
|
|
540
|
+
scored.forEach(s => {
|
|
541
|
+
debug(` port=${s.server.port} score=${s.score.toFixed(0)} [${s.reasons.join(', ')}]`);
|
|
542
|
+
});
|
|
543
|
+
return scored.map(s => s.server);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Try servers one by one until success or all fail
|
|
547
|
+
*/
|
|
548
|
+
async function tryServersSequentially(servers, projectDirectory, summary, timeout, agentName) {
|
|
549
|
+
for (const server of servers) {
|
|
550
|
+
// Skip already tried servers
|
|
551
|
+
if (triedServerPorts.has(server.port)) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
triedServerPorts.add(server.port);
|
|
555
|
+
debug(`Trying server port ${server.port}...`);
|
|
556
|
+
try {
|
|
557
|
+
const result = await tryServerOnce(server, projectDirectory, summary, timeout, agentName);
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
const errorMessage = err.message || '';
|
|
562
|
+
// These errors mean we should try next server
|
|
563
|
+
if (errorMessage === 'PROJECT_NOT_IN_WINDOW' ||
|
|
564
|
+
errorMessage.includes('No feedback panel') ||
|
|
565
|
+
errorMessage.includes('Connection failed')) {
|
|
566
|
+
debug(`Server ${server.port} rejected: ${errorMessage}, trying next...`);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
// Other errors (timeout, user cancelled) should propagate
|
|
570
|
+
throw err;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// All servers failed, fall back to browser
|
|
574
|
+
debug('All servers failed, falling back to browser');
|
|
575
|
+
return requestFeedbackViaBrowser(projectDirectory, summary, timeout);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Try a single server, returns result or throws error
|
|
579
|
+
*/
|
|
580
|
+
async function tryServerOnce(server, projectDirectory, summary, timeout, agentName) {
|
|
581
|
+
const url = `ws://127.0.0.1:${server.port}/ws`;
|
|
420
582
|
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
421
583
|
return new Promise((resolve, reject) => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
584
|
+
let conn = null;
|
|
585
|
+
let resolved = false;
|
|
586
|
+
const cleanup = () => {
|
|
587
|
+
if (conn && conn.readyState === WebSocket.OPEN) {
|
|
588
|
+
conn.close();
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
// Timeout for this attempt (use a shorter timeout for connection phase)
|
|
592
|
+
const connectionTimeout = setTimeout(() => {
|
|
593
|
+
if (!resolved) {
|
|
594
|
+
resolved = true;
|
|
595
|
+
cleanup();
|
|
596
|
+
reject(new Error('Connection failed'));
|
|
597
|
+
}
|
|
598
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
599
|
+
// Overall timeout
|
|
600
|
+
const overallTimeout = setTimeout(() => {
|
|
601
|
+
if (!resolved) {
|
|
602
|
+
resolved = true;
|
|
603
|
+
cleanup();
|
|
604
|
+
reject(new Error(`Feedback timeout after ${timeout} seconds`));
|
|
605
|
+
}
|
|
426
606
|
}, timeout * 1000);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
607
|
+
try {
|
|
608
|
+
conn = new WebSocket(url);
|
|
609
|
+
conn.on('open', () => {
|
|
610
|
+
clearTimeout(connectionTimeout);
|
|
611
|
+
debug(`Connected to server port ${server.port}`);
|
|
612
|
+
// Register
|
|
613
|
+
conn.send(JSON.stringify({
|
|
614
|
+
type: 'register',
|
|
615
|
+
clientType: 'mcp-server',
|
|
616
|
+
pid: process.pid,
|
|
617
|
+
parentPid: process.ppid
|
|
618
|
+
}));
|
|
619
|
+
// Send feedback request
|
|
620
|
+
conn.send(JSON.stringify({
|
|
621
|
+
type: 'feedback_request',
|
|
622
|
+
session_id: sessionId,
|
|
623
|
+
project_directory: projectDirectory,
|
|
624
|
+
summary,
|
|
625
|
+
timeout,
|
|
626
|
+
agent_name: agentName
|
|
627
|
+
}));
|
|
628
|
+
debug(`Feedback request sent: session=${sessionId}`);
|
|
629
|
+
});
|
|
630
|
+
conn.on('message', (data) => {
|
|
631
|
+
try {
|
|
632
|
+
const message = JSON.parse(data.toString());
|
|
633
|
+
debug(`Received from port ${server.port}: ${message.type}`);
|
|
634
|
+
switch (message.type) {
|
|
635
|
+
case 'feedback_result':
|
|
636
|
+
if (message.session_id === sessionId && !resolved) {
|
|
637
|
+
resolved = true;
|
|
638
|
+
clearTimeout(overallTimeout);
|
|
639
|
+
cleanup();
|
|
640
|
+
resolve({
|
|
641
|
+
feedback: message.feedback,
|
|
642
|
+
images: message.images || []
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
break;
|
|
646
|
+
case 'feedback_error':
|
|
647
|
+
if (message.session_id === sessionId && !resolved) {
|
|
648
|
+
resolved = true;
|
|
649
|
+
clearTimeout(overallTimeout);
|
|
650
|
+
cleanup();
|
|
651
|
+
reject(new Error(message.error));
|
|
652
|
+
}
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch (e) {
|
|
657
|
+
debug(`Parse error: ${e}`);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
conn.on('error', (err) => {
|
|
661
|
+
if (!resolved) {
|
|
662
|
+
resolved = true;
|
|
663
|
+
clearTimeout(connectionTimeout);
|
|
664
|
+
clearTimeout(overallTimeout);
|
|
665
|
+
reject(new Error(`Connection failed: ${err.message}`));
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
conn.on('close', () => {
|
|
669
|
+
if (!resolved) {
|
|
670
|
+
resolved = true;
|
|
671
|
+
clearTimeout(connectionTimeout);
|
|
672
|
+
clearTimeout(overallTimeout);
|
|
673
|
+
reject(new Error('Connection closed'));
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (e) {
|
|
678
|
+
if (!resolved) {
|
|
679
|
+
resolved = true;
|
|
680
|
+
clearTimeout(connectionTimeout);
|
|
681
|
+
clearTimeout(overallTimeout);
|
|
682
|
+
reject(new Error(`Connection failed: ${e}`));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
// ============================================================================
|
|
688
|
+
// Browser Fallback
|
|
689
|
+
// ============================================================================
|
|
690
|
+
/**
|
|
691
|
+
* Open system default browser
|
|
692
|
+
*/
|
|
693
|
+
function openBrowser(url) {
|
|
694
|
+
const platform = os.platform();
|
|
695
|
+
let cmd;
|
|
696
|
+
if (platform === 'darwin') {
|
|
697
|
+
cmd = `open "${url}"`;
|
|
698
|
+
}
|
|
699
|
+
else if (platform === 'win32') {
|
|
700
|
+
cmd = `start "" "${url}"`;
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
cmd = `xdg-open "${url}"`;
|
|
704
|
+
}
|
|
705
|
+
exec(cmd, (error) => {
|
|
706
|
+
if (error) {
|
|
707
|
+
debug(`Failed to open browser: ${error.message}`);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Generate HTML page for browser feedback
|
|
713
|
+
*/
|
|
714
|
+
function generateBrowserHtml(summary, port) {
|
|
715
|
+
const escapedSummary = summary
|
|
716
|
+
.replace(/&/g, '&')
|
|
717
|
+
.replace(/</g, '<')
|
|
718
|
+
.replace(/>/g, '>')
|
|
719
|
+
.replace(/"/g, '"')
|
|
720
|
+
.replace(/\n/g, '<br>');
|
|
721
|
+
return `<!DOCTYPE html>
|
|
722
|
+
<html lang="en">
|
|
723
|
+
<head>
|
|
724
|
+
<meta charset="UTF-8">
|
|
725
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
726
|
+
<title>MCP Feedback</title>
|
|
727
|
+
<style>
|
|
728
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
729
|
+
body {
|
|
730
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
731
|
+
background: #1e1e1e;
|
|
732
|
+
color: #d4d4d4;
|
|
733
|
+
min-height: 100vh;
|
|
734
|
+
display: flex;
|
|
735
|
+
align-items: center;
|
|
736
|
+
justify-content: center;
|
|
737
|
+
padding: 20px;
|
|
738
|
+
}
|
|
739
|
+
.container {
|
|
740
|
+
max-width: 800px;
|
|
741
|
+
width: 100%;
|
|
742
|
+
background: #252526;
|
|
743
|
+
border-radius: 12px;
|
|
744
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
745
|
+
overflow: hidden;
|
|
746
|
+
}
|
|
747
|
+
.header {
|
|
748
|
+
background: #007acc;
|
|
749
|
+
padding: 16px 24px;
|
|
750
|
+
display: flex;
|
|
751
|
+
align-items: center;
|
|
752
|
+
gap: 12px;
|
|
753
|
+
}
|
|
754
|
+
.header h1 {
|
|
755
|
+
font-size: 18px;
|
|
756
|
+
font-weight: 600;
|
|
757
|
+
color: white;
|
|
758
|
+
}
|
|
759
|
+
.header .icon { font-size: 24px; }
|
|
760
|
+
.content { padding: 24px; }
|
|
761
|
+
.ai-message {
|
|
762
|
+
background: #2d2d2d;
|
|
763
|
+
border: 1px solid #3c3c3c;
|
|
764
|
+
border-radius: 8px;
|
|
765
|
+
padding: 16px;
|
|
766
|
+
margin-bottom: 20px;
|
|
767
|
+
max-height: 400px;
|
|
768
|
+
overflow-y: auto;
|
|
769
|
+
line-height: 1.6;
|
|
770
|
+
}
|
|
771
|
+
.label {
|
|
772
|
+
font-size: 12px;
|
|
773
|
+
color: #888;
|
|
774
|
+
margin-bottom: 8px;
|
|
775
|
+
display: flex;
|
|
776
|
+
align-items: center;
|
|
777
|
+
gap: 6px;
|
|
778
|
+
}
|
|
779
|
+
.quick-btns {
|
|
780
|
+
display: flex;
|
|
781
|
+
gap: 8px;
|
|
782
|
+
margin-bottom: 16px;
|
|
783
|
+
flex-wrap: wrap;
|
|
784
|
+
}
|
|
785
|
+
.quick-btn {
|
|
786
|
+
padding: 8px 16px;
|
|
787
|
+
background: transparent;
|
|
788
|
+
border: 1px solid #3c3c3c;
|
|
789
|
+
color: #d4d4d4;
|
|
790
|
+
border-radius: 20px;
|
|
791
|
+
cursor: pointer;
|
|
792
|
+
font-size: 13px;
|
|
793
|
+
transition: all 0.2s;
|
|
794
|
+
}
|
|
795
|
+
.quick-btn:hover {
|
|
796
|
+
background: #3c3c3c;
|
|
797
|
+
border-color: #569cd6;
|
|
798
|
+
}
|
|
799
|
+
textarea {
|
|
800
|
+
width: 100%;
|
|
801
|
+
min-height: 120px;
|
|
802
|
+
padding: 12px;
|
|
803
|
+
background: #2d2d2d;
|
|
804
|
+
border: 1px solid #3c3c3c;
|
|
805
|
+
border-radius: 8px;
|
|
806
|
+
color: #d4d4d4;
|
|
807
|
+
font-size: 14px;
|
|
808
|
+
font-family: inherit;
|
|
809
|
+
resize: vertical;
|
|
810
|
+
margin-bottom: 16px;
|
|
811
|
+
}
|
|
812
|
+
textarea:focus {
|
|
813
|
+
outline: none;
|
|
814
|
+
border-color: #007acc;
|
|
815
|
+
}
|
|
816
|
+
.submit-btn {
|
|
817
|
+
width: 100%;
|
|
818
|
+
padding: 14px;
|
|
819
|
+
background: #007acc;
|
|
820
|
+
border: none;
|
|
821
|
+
border-radius: 8px;
|
|
822
|
+
color: white;
|
|
823
|
+
font-size: 15px;
|
|
824
|
+
font-weight: 600;
|
|
825
|
+
cursor: pointer;
|
|
826
|
+
transition: background 0.2s;
|
|
827
|
+
}
|
|
828
|
+
.submit-btn:hover { background: #0098ff; }
|
|
829
|
+
.submit-btn:disabled {
|
|
830
|
+
background: #3c3c3c;
|
|
831
|
+
cursor: not-allowed;
|
|
832
|
+
}
|
|
833
|
+
.success {
|
|
834
|
+
text-align: center;
|
|
835
|
+
padding: 40px;
|
|
836
|
+
}
|
|
837
|
+
.success .icon { font-size: 48px; margin-bottom: 16px; }
|
|
838
|
+
.success h2 { color: #4ec9b0; margin-bottom: 8px; }
|
|
839
|
+
.success p { color: #888; }
|
|
840
|
+
</style>
|
|
841
|
+
</head>
|
|
842
|
+
<body>
|
|
843
|
+
<div class="container" id="feedbackForm">
|
|
844
|
+
<div class="header">
|
|
845
|
+
<span class="icon">💬</span>
|
|
846
|
+
<h1>MCP Feedback</h1>
|
|
847
|
+
</div>
|
|
848
|
+
<div class="content">
|
|
849
|
+
<div class="label">🤖 AI Summary</div>
|
|
850
|
+
<div class="ai-message">${escapedSummary}</div>
|
|
851
|
+
|
|
852
|
+
<div class="label">💬 Your Feedback</div>
|
|
853
|
+
<div class="quick-btns">
|
|
854
|
+
<button class="quick-btn" onclick="setFeedback('Continue')">▶️ Continue</button>
|
|
855
|
+
<button class="quick-btn" onclick="setFeedback('Looks good')">👍 Good</button>
|
|
856
|
+
<button class="quick-btn" onclick="setFeedback('Please fix it')">🔧 Fix</button>
|
|
857
|
+
<button class="quick-btn" onclick="setFeedback('Stop')">⏹️ Stop</button>
|
|
858
|
+
</div>
|
|
859
|
+
<textarea id="feedback" placeholder="Type your feedback here..."></textarea>
|
|
860
|
+
<button class="submit-btn" id="submitBtn" onclick="submitFeedback()">Send Feedback</button>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
|
|
864
|
+
<div class="container" id="successMsg" style="display: none;">
|
|
865
|
+
<div class="success">
|
|
866
|
+
<div class="icon">✅</div>
|
|
867
|
+
<h2>Feedback Sent!</h2>
|
|
868
|
+
<p>You can close this window now.</p>
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
|
|
872
|
+
<script>
|
|
873
|
+
function setFeedback(text) {
|
|
874
|
+
document.getElementById('feedback').value = text;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function submitFeedback() {
|
|
878
|
+
const feedback = document.getElementById('feedback').value.trim();
|
|
879
|
+
if (!feedback) {
|
|
880
|
+
alert('Please enter your feedback');
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const btn = document.getElementById('submitBtn');
|
|
885
|
+
btn.disabled = true;
|
|
886
|
+
btn.textContent = 'Sending...';
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
const response = await fetch('/submit', {
|
|
890
|
+
method: 'POST',
|
|
891
|
+
headers: { 'Content-Type': 'application/json' },
|
|
892
|
+
body: JSON.stringify({ feedback })
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
if (response.ok) {
|
|
896
|
+
document.getElementById('feedbackForm').style.display = 'none';
|
|
897
|
+
document.getElementById('successMsg').style.display = 'block';
|
|
898
|
+
} else {
|
|
899
|
+
throw new Error('Submit failed');
|
|
900
|
+
}
|
|
901
|
+
} catch (e) {
|
|
902
|
+
btn.disabled = false;
|
|
903
|
+
btn.textContent = 'Send Feedback';
|
|
904
|
+
alert('Failed to send feedback. Please try again.');
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
</script>
|
|
908
|
+
</body>
|
|
909
|
+
</html>`;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Request feedback via browser (fallback when extension not available)
|
|
913
|
+
*/
|
|
914
|
+
async function requestFeedbackViaBrowser(projectDirectory, summary, timeout) {
|
|
915
|
+
return new Promise((resolve, reject) => {
|
|
916
|
+
// Find available port
|
|
917
|
+
const server = http.createServer();
|
|
918
|
+
server.listen(0, '127.0.0.1', () => {
|
|
919
|
+
const address = server.address();
|
|
920
|
+
if (!address || typeof address === 'string') {
|
|
921
|
+
reject(new Error('Failed to start HTTP server'));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const port = address.port;
|
|
925
|
+
debug(`Browser feedback server started on port ${port}`);
|
|
926
|
+
let feedbackReceived = false;
|
|
927
|
+
// Handle requests
|
|
928
|
+
server.on('request', (req, res) => {
|
|
929
|
+
if (req.method === 'GET' && req.url === '/') {
|
|
930
|
+
// Serve HTML page
|
|
931
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
932
|
+
res.end(generateBrowserHtml(summary, port));
|
|
933
|
+
}
|
|
934
|
+
else if (req.method === 'POST' && req.url === '/submit') {
|
|
935
|
+
// Handle feedback submission
|
|
936
|
+
let body = '';
|
|
937
|
+
req.on('data', chunk => body += chunk);
|
|
938
|
+
req.on('end', () => {
|
|
939
|
+
try {
|
|
940
|
+
const data = JSON.parse(body);
|
|
941
|
+
feedbackReceived = true;
|
|
942
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
943
|
+
res.end(JSON.stringify({ success: true }));
|
|
944
|
+
// Close server and resolve
|
|
945
|
+
setTimeout(() => {
|
|
946
|
+
server.close();
|
|
947
|
+
resolve({
|
|
948
|
+
feedback: data.feedback || '',
|
|
949
|
+
images: []
|
|
950
|
+
});
|
|
951
|
+
}, 1000);
|
|
952
|
+
}
|
|
953
|
+
catch (e) {
|
|
954
|
+
res.writeHead(400);
|
|
955
|
+
res.end('Invalid request');
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
res.writeHead(404);
|
|
961
|
+
res.end('Not found');
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
// Open browser
|
|
965
|
+
const url = `http://127.0.0.1:${port}`;
|
|
966
|
+
debug(`Opening browser: ${url}`);
|
|
967
|
+
openBrowser(url);
|
|
968
|
+
// Timeout
|
|
969
|
+
const timeoutHandle = setTimeout(() => {
|
|
970
|
+
if (!feedbackReceived) {
|
|
971
|
+
server.close();
|
|
972
|
+
reject(new Error(`Browser feedback timeout after ${timeout} seconds`));
|
|
973
|
+
}
|
|
974
|
+
}, timeout * 1000);
|
|
975
|
+
server.on('close', () => {
|
|
976
|
+
clearTimeout(timeoutHandle);
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
server.on('error', (err) => {
|
|
980
|
+
reject(new Error(`Failed to start browser feedback server: ${err.message}`));
|
|
432
981
|
});
|
|
433
|
-
// Send request to Extension
|
|
434
|
-
ws?.send(JSON.stringify({
|
|
435
|
-
type: 'feedback_request',
|
|
436
|
-
session_id: sessionId,
|
|
437
|
-
project_directory: projectDirectory,
|
|
438
|
-
summary,
|
|
439
|
-
timeout,
|
|
440
|
-
agent_name: agentName // For multi-agent display
|
|
441
|
-
}));
|
|
442
|
-
debug(`Feedback request sent: session=${sessionId}, agent=${agentName || 'default'}`);
|
|
443
982
|
});
|
|
444
983
|
}
|
|
445
984
|
// ============================================================================
|