cursor-feedback 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1037 @@
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.activate = activate;
37
+ exports.deactivate = deactivate;
38
+ const vscode = __importStar(require("vscode"));
39
+ const http = __importStar(require("http"));
40
+ let feedbackViewProvider = null;
41
+ let pollingInterval = null;
42
+ function activate(context) {
43
+ console.log('Cursor Feedback extension is now active!');
44
+ const config = vscode.workspace.getConfiguration('cursorFeedback');
45
+ const port = config.get('serverPort', 5678);
46
+ // 注册侧边栏 WebView
47
+ feedbackViewProvider = new FeedbackViewProvider(context.extensionUri, port);
48
+ context.subscriptions.push(vscode.window.registerWebviewViewProvider('cursorFeedback.feedbackView', feedbackViewProvider));
49
+ // 注册命令:显示反馈面板
50
+ context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.showPanel', () => {
51
+ vscode.commands.executeCommand('cursorFeedback.feedbackView.focus');
52
+ }));
53
+ // 注册命令:启动轮询
54
+ context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.startPolling', () => {
55
+ if (feedbackViewProvider) {
56
+ feedbackViewProvider.startPolling();
57
+ vscode.window.showInformationMessage('开始监听 MCP 反馈请求');
58
+ }
59
+ }));
60
+ // 注册命令:停止轮询
61
+ context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.stopPolling', () => {
62
+ if (feedbackViewProvider) {
63
+ feedbackViewProvider.stopPolling();
64
+ vscode.window.showInformationMessage('已停止监听');
65
+ }
66
+ }));
67
+ // 自动开始轮询
68
+ if (config.get('autoStartServer', true)) {
69
+ setTimeout(() => {
70
+ feedbackViewProvider?.startPolling();
71
+ }, 1000);
72
+ }
73
+ }
74
+ function deactivate() {
75
+ if (feedbackViewProvider) {
76
+ feedbackViewProvider.stopPolling();
77
+ }
78
+ }
79
+ /**
80
+ * 获取当前工作区路径列表
81
+ */
82
+ function getWorkspacePaths() {
83
+ const folders = vscode.workspace.workspaceFolders;
84
+ if (!folders) {
85
+ return [];
86
+ }
87
+ return folders.map(f => f.uri.fsPath);
88
+ }
89
+ /**
90
+ * 检查路径是否匹配当前工作区(精确匹配)
91
+ */
92
+ function isPathInWorkspace(targetPath) {
93
+ const workspacePaths = getWorkspacePaths();
94
+ if (workspacePaths.length === 0) {
95
+ return true; // 没有打开工作区时,接受所有请求
96
+ }
97
+ // 规范化路径(去除末尾斜杠,统一分隔符,小写)
98
+ const normalize = (p) => p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
99
+ const normalizedTarget = normalize(targetPath);
100
+ for (const wsPath of workspacePaths) {
101
+ const normalizedWs = normalize(wsPath);
102
+ // 精确匹配:只匹配完全相同的路径
103
+ if (normalizedTarget === normalizedWs) {
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+ /**
110
+ * 侧边栏 WebView Provider
111
+ */
112
+ class FeedbackViewProvider {
113
+ constructor(_extensionUri, port) {
114
+ this._extensionUri = _extensionUri;
115
+ this._pollingInterval = null;
116
+ this._currentRequest = null;
117
+ this._activePort = null;
118
+ this._portScanRange = 10; // 扫描端口范围
119
+ this._basePort = port;
120
+ }
121
+ resolveWebviewView(webviewView, _context, _token) {
122
+ this._view = webviewView;
123
+ webviewView.webview.options = {
124
+ enableScripts: true,
125
+ localResourceRoots: [this._extensionUri]
126
+ };
127
+ webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
128
+ // 处理来自 WebView 的消息
129
+ webviewView.webview.onDidReceiveMessage(async (data) => {
130
+ switch (data.type) {
131
+ case 'submitFeedback':
132
+ await this._handleFeedbackSubmit(data.payload);
133
+ break;
134
+ case 'ready':
135
+ console.log('Feedback WebView is ready');
136
+ // WebView 准备就绪后,检查是否有待处理的请求
137
+ if (this._currentRequest) {
138
+ this._showFeedbackRequest(this._currentRequest);
139
+ }
140
+ break;
141
+ case 'checkServer':
142
+ await this._checkServerHealth();
143
+ break;
144
+ }
145
+ });
146
+ // 当 view 变为可见时,检查当前请求
147
+ webviewView.onDidChangeVisibility(() => {
148
+ if (webviewView.visible && this._currentRequest) {
149
+ this._showFeedbackRequest(this._currentRequest);
150
+ }
151
+ });
152
+ }
153
+ /**
154
+ * 开始轮询 MCP Server
155
+ */
156
+ startPolling() {
157
+ if (this._pollingInterval) {
158
+ return;
159
+ }
160
+ console.log(`Starting polling MCP server from port ${this._basePort}`);
161
+ this._pollingInterval = setInterval(async () => {
162
+ await this._pollForFeedbackRequest();
163
+ }, 1000); // 每秒检查一次
164
+ // 立即执行一次
165
+ this._pollForFeedbackRequest();
166
+ }
167
+ /**
168
+ * 停止轮询
169
+ */
170
+ stopPolling() {
171
+ if (this._pollingInterval) {
172
+ clearInterval(this._pollingInterval);
173
+ this._pollingInterval = null;
174
+ }
175
+ }
176
+ /**
177
+ * 轮询检查是否有新的反馈请求
178
+ * 会扫描多个端口以找到有活跃请求的 MCP Server
179
+ */
180
+ async _pollForFeedbackRequest() {
181
+ try {
182
+ // 如果已知活跃端口,先检查该端口
183
+ if (this._activePort) {
184
+ const result = await this._checkPortForRequest(this._activePort);
185
+ if (result.request) {
186
+ this._handleNewRequest(result.request, result.port);
187
+ return;
188
+ }
189
+ else if (result.connected && !result.request && this._currentRequest) {
190
+ // 请求已被处理或超时
191
+ this._currentRequest = null;
192
+ this._showWaitingState();
193
+ return;
194
+ }
195
+ }
196
+ // 扫描端口范围寻找有请求的服务器
197
+ for (let i = 0; i < this._portScanRange; i++) {
198
+ const port = this._basePort + i;
199
+ if (port === this._activePort)
200
+ continue; // 已经检查过了
201
+ const result = await this._checkPortForRequest(port);
202
+ if (result.request) {
203
+ this._activePort = port;
204
+ this._handleNewRequest(result.request, port);
205
+ return;
206
+ }
207
+ }
208
+ // 没有找到任何请求
209
+ if (this._currentRequest) {
210
+ this._currentRequest = null;
211
+ this._showWaitingState();
212
+ }
213
+ }
214
+ catch (error) {
215
+ // 服务器可能未启动,静默处理
216
+ }
217
+ }
218
+ /**
219
+ * 检查指定端口是否有反馈请求
220
+ * 只返回属于当前工作区的请求
221
+ */
222
+ async _checkPortForRequest(port) {
223
+ try {
224
+ const response = await this._httpGet(`http://127.0.0.1:${port}/api/feedback/current`);
225
+ const request = JSON.parse(response);
226
+ // 如果有请求,检查是否属于当前工作区
227
+ if (request && !isPathInWorkspace(request.projectDir)) {
228
+ // 请求不属于当前工作区,忽略
229
+ return { connected: true, request: null, port };
230
+ }
231
+ return { connected: true, request, port };
232
+ }
233
+ catch {
234
+ return { connected: false, request: null, port };
235
+ }
236
+ }
237
+ /**
238
+ * 处理新的反馈请求
239
+ */
240
+ _handleNewRequest(request, port) {
241
+ if (!this._currentRequest || request.id !== this._currentRequest.id) {
242
+ console.log(`New feedback request received on port ${port}:`, request.id);
243
+ this._currentRequest = request;
244
+ this._activePort = port;
245
+ this._showFeedbackRequest(request);
246
+ // 显示通知
247
+ vscode.window.showInformationMessage('AI 正在等待您的反馈', '查看').then(action => {
248
+ if (action === '查看') {
249
+ vscode.commands.executeCommand('cursorFeedback.feedbackView.focus');
250
+ }
251
+ });
252
+ }
253
+ }
254
+ /**
255
+ * 检查服务器健康状态
256
+ */
257
+ async _checkServerHealth() {
258
+ // 扫描端口查找可用的服务器
259
+ for (let i = 0; i < this._portScanRange; i++) {
260
+ const port = this._basePort + i;
261
+ try {
262
+ const response = await this._httpGet(`http://127.0.0.1:${port}/api/health`);
263
+ const health = JSON.parse(response);
264
+ this._view?.webview.postMessage({
265
+ type: 'serverStatus',
266
+ payload: { connected: true, port, ...health }
267
+ });
268
+ return;
269
+ }
270
+ catch {
271
+ // 继续尝试下一个端口
272
+ }
273
+ }
274
+ this._view?.webview.postMessage({
275
+ type: 'serverStatus',
276
+ payload: { connected: false }
277
+ });
278
+ }
279
+ /**
280
+ * 显示反馈请求
281
+ */
282
+ _showFeedbackRequest(request) {
283
+ if (this._view) {
284
+ this._view.show?.(true);
285
+ this._view.webview.postMessage({
286
+ type: 'showFeedbackRequest',
287
+ payload: {
288
+ requestId: request.id,
289
+ summary: request.summary,
290
+ projectDir: request.projectDir,
291
+ timeout: request.timeout,
292
+ timestamp: request.timestamp
293
+ }
294
+ });
295
+ }
296
+ }
297
+ /**
298
+ * 显示等待状态
299
+ */
300
+ _showWaitingState() {
301
+ if (this._view) {
302
+ this._view.webview.postMessage({
303
+ type: 'showWaiting'
304
+ });
305
+ }
306
+ }
307
+ /**
308
+ * 处理反馈提交
309
+ */
310
+ async _handleFeedbackSubmit(payload) {
311
+ // 使用活跃端口提交反馈
312
+ const port = this._activePort || this._basePort;
313
+ try {
314
+ const response = await this._httpPost(`http://127.0.0.1:${port}/api/feedback/submit`, JSON.stringify({
315
+ requestId: payload.requestId,
316
+ feedback: {
317
+ interactive_feedback: payload.interactive_feedback,
318
+ images: payload.images,
319
+ project_directory: payload.project_directory
320
+ }
321
+ }));
322
+ const result = JSON.parse(response);
323
+ if (result.success) {
324
+ vscode.window.showInformationMessage('反馈已提交');
325
+ this._currentRequest = null;
326
+ this._showWaitingState();
327
+ }
328
+ else {
329
+ vscode.window.showErrorMessage('提交失败:' + result.error);
330
+ }
331
+ }
332
+ catch (error) {
333
+ vscode.window.showErrorMessage('提交失败:无法连接到 MCP Server');
334
+ }
335
+ }
336
+ /**
337
+ * HTTP GET 请求
338
+ */
339
+ _httpGet(url) {
340
+ return new Promise((resolve, reject) => {
341
+ const req = http.get(url, { timeout: 3000 }, (res) => {
342
+ let data = '';
343
+ res.on('data', chunk => data += chunk);
344
+ res.on('end', () => resolve(data));
345
+ });
346
+ req.on('error', reject);
347
+ req.on('timeout', () => {
348
+ req.destroy();
349
+ reject(new Error('Request timeout'));
350
+ });
351
+ });
352
+ }
353
+ /**
354
+ * HTTP POST 请求
355
+ */
356
+ _httpPost(url, body) {
357
+ return new Promise((resolve, reject) => {
358
+ const urlObj = new URL(url);
359
+ const options = {
360
+ hostname: urlObj.hostname,
361
+ port: urlObj.port,
362
+ path: urlObj.pathname,
363
+ method: 'POST',
364
+ headers: {
365
+ 'Content-Type': 'application/json',
366
+ 'Content-Length': Buffer.byteLength(body)
367
+ },
368
+ timeout: 5000
369
+ };
370
+ const req = http.request(options, (res) => {
371
+ let data = '';
372
+ res.on('data', chunk => data += chunk);
373
+ res.on('end', () => resolve(data));
374
+ });
375
+ req.on('error', reject);
376
+ req.on('timeout', () => {
377
+ req.destroy();
378
+ reject(new Error('Request timeout'));
379
+ });
380
+ req.write(body);
381
+ req.end();
382
+ });
383
+ }
384
+ _getHtmlForWebview(webview) {
385
+ const config = vscode.workspace.getConfiguration('cursorFeedback');
386
+ const language = config.get('language', 'zh-CN');
387
+ const i18n = this._getI18n(language);
388
+ // 获取本地 marked.js 文件的 URI
389
+ const markedJsUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'resources', 'vendor', 'marked.min.js'));
390
+ return `<!DOCTYPE html>
391
+ <html lang="${language}">
392
+ <head>
393
+ <meta charset="UTF-8">
394
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
395
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' ${webview.cspSource}; img-src data:;">
396
+ <title>Cursor Feedback</title>
397
+ <!-- 使用本地 marked.js 进行 Markdown 渲染 -->
398
+ <script src="${markedJsUri}"></script>
399
+ <style>
400
+ * {
401
+ box-sizing: border-box;
402
+ margin: 0;
403
+ padding: 0;
404
+ }
405
+
406
+ body {
407
+ font-family: var(--vscode-font-family);
408
+ font-size: var(--vscode-font-size);
409
+ color: var(--vscode-foreground);
410
+ background-color: var(--vscode-sideBar-background);
411
+ padding: 12px;
412
+ min-height: 100vh;
413
+ }
414
+
415
+ .container {
416
+ display: flex;
417
+ flex-direction: column;
418
+ gap: 12px;
419
+ }
420
+
421
+ .section {
422
+ background: var(--vscode-input-background);
423
+ border: 1px solid var(--vscode-input-border);
424
+ border-radius: 6px;
425
+ padding: 12px;
426
+ }
427
+
428
+ .section-title {
429
+ font-weight: 600;
430
+ margin-bottom: 8px;
431
+ color: var(--vscode-foreground);
432
+ display: flex;
433
+ align-items: center;
434
+ gap: 6px;
435
+ }
436
+
437
+ .summary-content {
438
+ word-break: break-word;
439
+ max-height: 300px;
440
+ overflow-y: auto;
441
+ font-size: 13px;
442
+ line-height: 1.6;
443
+ background: var(--vscode-textBlockQuote-background);
444
+ padding: 12px;
445
+ border-radius: 4px;
446
+ }
447
+
448
+ /* Markdown 样式 */
449
+ .summary-content h1, .summary-content h2, .summary-content h3 {
450
+ margin-top: 12px;
451
+ margin-bottom: 8px;
452
+ font-weight: 600;
453
+ color: var(--vscode-foreground);
454
+ }
455
+ .summary-content h1 { font-size: 1.4em; border-bottom: 1px solid var(--vscode-panel-border); padding-bottom: 4px; }
456
+ .summary-content h2 { font-size: 1.2em; }
457
+ .summary-content h3 { font-size: 1.1em; }
458
+ .summary-content h1:first-child, .summary-content h2:first-child, .summary-content h3:first-child { margin-top: 0; }
459
+
460
+ .summary-content p { margin: 8px 0; }
461
+ .summary-content ul, .summary-content ol { margin: 8px 0; padding-left: 20px; }
462
+ .summary-content li { margin: 4px 0; }
463
+
464
+ .summary-content code {
465
+ background: var(--vscode-textCodeBlock-background);
466
+ padding: 2px 6px;
467
+ border-radius: 3px;
468
+ font-family: var(--vscode-editor-font-family), monospace;
469
+ font-size: 0.9em;
470
+ }
471
+
472
+ .summary-content pre {
473
+ background: var(--vscode-textCodeBlock-background);
474
+ padding: 10px;
475
+ border-radius: 4px;
476
+ overflow-x: auto;
477
+ margin: 8px 0;
478
+ }
479
+ .summary-content pre code {
480
+ background: none;
481
+ padding: 0;
482
+ }
483
+
484
+ .summary-content blockquote {
485
+ border-left: 3px solid var(--vscode-textLink-foreground);
486
+ margin: 8px 0;
487
+ padding: 4px 12px;
488
+ color: var(--vscode-descriptionForeground);
489
+ }
490
+
491
+ .summary-content table {
492
+ border-collapse: collapse;
493
+ margin: 8px 0;
494
+ width: 100%;
495
+ }
496
+ .summary-content th, .summary-content td {
497
+ border: 1px solid var(--vscode-panel-border);
498
+ padding: 6px 10px;
499
+ text-align: left;
500
+ }
501
+ .summary-content th {
502
+ background: var(--vscode-textCodeBlock-background);
503
+ }
504
+
505
+ .summary-content strong { font-weight: 600; }
506
+ .summary-content em { font-style: italic; }
507
+ .summary-content a { color: var(--vscode-textLink-foreground); }
508
+ .summary-content hr { border: none; border-top: 1px solid var(--vscode-panel-border); margin: 12px 0; }
509
+
510
+ .feedback-input {
511
+ width: 100%;
512
+ min-height: 120px;
513
+ resize: vertical;
514
+ background: var(--vscode-input-background);
515
+ color: var(--vscode-input-foreground);
516
+ border: 1px solid var(--vscode-input-border);
517
+ border-radius: 4px;
518
+ padding: 10px;
519
+ font-family: inherit;
520
+ font-size: 13px;
521
+ line-height: 1.5;
522
+ }
523
+
524
+ .feedback-input:focus {
525
+ outline: none;
526
+ border-color: var(--vscode-focusBorder);
527
+ }
528
+
529
+ .quick-buttons {
530
+ display: flex;
531
+ flex-wrap: wrap;
532
+ gap: 6px;
533
+ margin-top: 10px;
534
+ }
535
+
536
+ .quick-btn {
537
+ padding: 5px 12px;
538
+ font-size: 12px;
539
+ background: var(--vscode-button-secondaryBackground);
540
+ color: var(--vscode-button-secondaryForeground);
541
+ border: none;
542
+ border-radius: 4px;
543
+ cursor: pointer;
544
+ transition: background 0.15s;
545
+ }
546
+
547
+ .quick-btn:hover {
548
+ background: var(--vscode-button-secondaryHoverBackground);
549
+ }
550
+
551
+ .submit-btn {
552
+ width: 100%;
553
+ padding: 12px;
554
+ background: var(--vscode-button-background);
555
+ color: var(--vscode-button-foreground);
556
+ border: none;
557
+ border-radius: 6px;
558
+ cursor: pointer;
559
+ font-weight: 600;
560
+ font-size: 14px;
561
+ transition: background 0.15s;
562
+ }
563
+
564
+ .submit-btn:hover {
565
+ background: var(--vscode-button-hoverBackground);
566
+ }
567
+
568
+ .submit-btn:disabled {
569
+ opacity: 0.5;
570
+ cursor: not-allowed;
571
+ }
572
+
573
+ .status {
574
+ text-align: center;
575
+ padding: 30px 20px;
576
+ color: var(--vscode-descriptionForeground);
577
+ }
578
+
579
+ .status-icon {
580
+ font-size: 32px;
581
+ margin-bottom: 12px;
582
+ }
583
+
584
+ .status.waiting .status-icon {
585
+ animation: pulse 2s ease-in-out infinite;
586
+ }
587
+
588
+ @keyframes pulse {
589
+ 0%, 100% { opacity: 1; }
590
+ 50% { opacity: 0.5; }
591
+ }
592
+
593
+ .server-status {
594
+ display: flex;
595
+ align-items: center;
596
+ gap: 6px;
597
+ font-size: 11px;
598
+ padding: 6px 10px;
599
+ background: var(--vscode-textBlockQuote-background);
600
+ border-radius: 4px;
601
+ margin-bottom: 12px;
602
+ }
603
+
604
+ .server-status .dot {
605
+ width: 8px;
606
+ height: 8px;
607
+ border-radius: 50%;
608
+ background: var(--vscode-errorForeground);
609
+ }
610
+
611
+ .server-status.connected .dot {
612
+ background: var(--vscode-notificationsInfoIcon-foreground);
613
+ }
614
+
615
+ .image-upload {
616
+ margin-top: 10px;
617
+ }
618
+
619
+ .image-upload-btn {
620
+ display: inline-flex;
621
+ align-items: center;
622
+ gap: 6px;
623
+ padding: 8px 14px;
624
+ background: var(--vscode-button-secondaryBackground);
625
+ color: var(--vscode-button-secondaryForeground);
626
+ border: none;
627
+ border-radius: 4px;
628
+ cursor: pointer;
629
+ font-size: 12px;
630
+ }
631
+
632
+ .image-upload-btn:hover {
633
+ background: var(--vscode-button-secondaryHoverBackground);
634
+ }
635
+
636
+ .image-preview {
637
+ display: flex;
638
+ flex-wrap: wrap;
639
+ gap: 8px;
640
+ margin-top: 10px;
641
+ }
642
+
643
+ .image-preview-item {
644
+ position: relative;
645
+ }
646
+
647
+ .image-preview img {
648
+ max-width: 80px;
649
+ max-height: 80px;
650
+ border-radius: 4px;
651
+ object-fit: cover;
652
+ border: 1px solid var(--vscode-input-border);
653
+ }
654
+
655
+ .image-remove {
656
+ position: absolute;
657
+ top: -6px;
658
+ right: -6px;
659
+ width: 18px;
660
+ height: 18px;
661
+ border-radius: 50%;
662
+ background: var(--vscode-errorForeground);
663
+ color: white;
664
+ border: none;
665
+ cursor: pointer;
666
+ font-size: 12px;
667
+ display: flex;
668
+ align-items: center;
669
+ justify-content: center;
670
+ }
671
+
672
+ .hidden {
673
+ display: none !important;
674
+ }
675
+
676
+ .project-info {
677
+ font-size: 11px;
678
+ color: var(--vscode-descriptionForeground);
679
+ margin-top: 6px;
680
+ padding: 6px 8px;
681
+ background: var(--vscode-textBlockQuote-background);
682
+ border-radius: 4px;
683
+ word-break: break-all;
684
+ }
685
+
686
+ .timeout-info {
687
+ font-size: 11px;
688
+ color: var(--vscode-descriptionForeground);
689
+ text-align: right;
690
+ margin-top: 6px;
691
+ }
692
+ </style>
693
+ </head>
694
+ <body>
695
+ <div class="container">
696
+ <!-- 服务器状态 -->
697
+ <div id="serverStatus" class="server-status">
698
+ <span class="dot"></span>
699
+ <span id="serverStatusText">${i18n.checking}</span>
700
+ </div>
701
+
702
+ <!-- 等待状态 -->
703
+ <div id="waitingStatus" class="status waiting">
704
+ <div class="status-icon">⏳</div>
705
+ <p>${i18n.waiting}</p>
706
+ <p style="font-size: 11px; margin-top: 10px; opacity: 0.8;">${i18n.waitingHint}</p>
707
+ </div>
708
+
709
+ <!-- 反馈表单 -->
710
+ <div id="feedbackForm" class="hidden">
711
+ <!-- AI 摘要 -->
712
+ <div class="section">
713
+ <div class="section-title">📋 ${i18n.summary}</div>
714
+ <div id="summaryContent" class="summary-content"></div>
715
+ <div id="projectInfo" class="project-info"></div>
716
+ </div>
717
+
718
+ <!-- 反馈输入 -->
719
+ <div class="section">
720
+ <div class="section-title">💬 ${i18n.yourFeedback}</div>
721
+ <textarea
722
+ id="feedbackInput"
723
+ class="feedback-input"
724
+ placeholder="${i18n.placeholder}"
725
+ ></textarea>
726
+
727
+ <!-- 快捷按钮 -->
728
+ <div class="quick-buttons">
729
+ <button class="quick-btn" data-text="${i18n.continue}">${i18n.continue}</button>
730
+ <button class="quick-btn" data-text="${i18n.confirm}">${i18n.confirm}</button>
731
+ <button class="quick-btn" data-text="${i18n.modify}">${i18n.modify}</button>
732
+ <button class="quick-btn" data-text="${i18n.cancel}">${i18n.cancel}</button>
733
+ </div>
734
+
735
+ <!-- 图片上传 -->
736
+ <div class="image-upload">
737
+ <button id="uploadBtn" class="image-upload-btn">
738
+ 📎 ${i18n.uploadImage}
739
+ </button>
740
+ <input type="file" id="imageInput" accept="image/*" multiple style="display:none">
741
+ <div id="imagePreview" class="image-preview"></div>
742
+ </div>
743
+
744
+ <div id="timeoutInfo" class="timeout-info"></div>
745
+ </div>
746
+
747
+ <!-- 提交按钮 -->
748
+ <button id="submitBtn" class="submit-btn">${i18n.submit} (Ctrl+Enter)</button>
749
+ </div>
750
+ </div>
751
+
752
+ <script>
753
+ const vscode = acquireVsCodeApi();
754
+
755
+ // 使用 marked.js 渲染 Markdown
756
+ function renderMarkdown(text) {
757
+ if (!text) return '';
758
+
759
+ try {
760
+ // 配置 marked
761
+ if (typeof marked !== 'undefined') {
762
+ marked.setOptions({
763
+ breaks: true, // 支持 GitHub 风格的换行
764
+ gfm: true, // 启用 GitHub 风格 Markdown
765
+ headerIds: false, // 禁用标题 ID(安全考虑)
766
+ });
767
+
768
+ // 使用 marked 解析
769
+ return marked.parse(text);
770
+ }
771
+ } catch (e) {
772
+ console.error('Markdown rendering error:', e);
773
+ }
774
+
775
+ // 降级:简单转义并保留换行
776
+ return text
777
+ .replace(/&/g, '&amp;')
778
+ .replace(/</g, '&lt;')
779
+ .replace(/>/g, '&gt;')
780
+ .replace(/\\n/g, '<br>');
781
+ }
782
+
783
+ // DOM 元素
784
+ const serverStatus = document.getElementById('serverStatus');
785
+ const serverStatusText = document.getElementById('serverStatusText');
786
+ const waitingStatus = document.getElementById('waitingStatus');
787
+ const feedbackForm = document.getElementById('feedbackForm');
788
+ const summaryContent = document.getElementById('summaryContent');
789
+ const projectInfo = document.getElementById('projectInfo');
790
+ const feedbackInput = document.getElementById('feedbackInput');
791
+ const submitBtn = document.getElementById('submitBtn');
792
+ const uploadBtn = document.getElementById('uploadBtn');
793
+ const imageInput = document.getElementById('imageInput');
794
+ const imagePreview = document.getElementById('imagePreview');
795
+ const timeoutInfo = document.getElementById('timeoutInfo');
796
+
797
+ let uploadedImages = [];
798
+ let currentRequestId = '';
799
+ let currentProjectDir = '';
800
+ let requestTimestamp = 0;
801
+ let requestTimeout = 600;
802
+ let countdownInterval = null;
803
+
804
+ // 国际化文本
805
+ const i18n = ${JSON.stringify(i18n)};
806
+
807
+ // 快捷按钮
808
+ document.querySelectorAll('.quick-btn').forEach(btn => {
809
+ btn.addEventListener('click', () => {
810
+ feedbackInput.value = btn.dataset.text;
811
+ feedbackInput.focus();
812
+ });
813
+ });
814
+
815
+ // 图片上传
816
+ uploadBtn.addEventListener('click', () => imageInput.click());
817
+
818
+ imageInput.addEventListener('change', (e) => {
819
+ const files = e.target.files;
820
+ for (const file of files) {
821
+ const reader = new FileReader();
822
+ reader.onload = (e) => {
823
+ const base64 = e.target.result;
824
+ const imgData = {
825
+ name: file.name,
826
+ data: base64.split(',')[1],
827
+ size: file.size
828
+ };
829
+ uploadedImages.push(imgData);
830
+
831
+ // 显示预览
832
+ const container = document.createElement('div');
833
+ container.className = 'image-preview-item';
834
+
835
+ const img = document.createElement('img');
836
+ img.src = base64;
837
+
838
+ const removeBtn = document.createElement('button');
839
+ removeBtn.className = 'image-remove';
840
+ removeBtn.textContent = '×';
841
+ removeBtn.onclick = () => {
842
+ const index = uploadedImages.indexOf(imgData);
843
+ if (index > -1) {
844
+ uploadedImages.splice(index, 1);
845
+ }
846
+ container.remove();
847
+ };
848
+
849
+ container.appendChild(img);
850
+ container.appendChild(removeBtn);
851
+ imagePreview.appendChild(container);
852
+ };
853
+ reader.readAsDataURL(file);
854
+ }
855
+ });
856
+
857
+ // 更新倒计时
858
+ function updateCountdown() {
859
+ if (!requestTimestamp || !requestTimeout) return;
860
+
861
+ const elapsed = Math.floor((Date.now() - requestTimestamp) / 1000);
862
+ const remaining = Math.max(0, requestTimeout - elapsed);
863
+ const minutes = Math.floor(remaining / 60);
864
+ const seconds = remaining % 60;
865
+
866
+ timeoutInfo.textContent = i18n.timeout + ': ' + minutes + ':' + seconds.toString().padStart(2, '0');
867
+
868
+ if (remaining <= 0) {
869
+ clearInterval(countdownInterval);
870
+ timeoutInfo.textContent = i18n.expired;
871
+ }
872
+ }
873
+
874
+ // 提交反馈
875
+ function submitFeedback() {
876
+ const feedback = feedbackInput.value.trim();
877
+
878
+ if (!currentRequestId) {
879
+ return;
880
+ }
881
+
882
+ vscode.postMessage({
883
+ type: 'submitFeedback',
884
+ payload: {
885
+ requestId: currentRequestId,
886
+ interactive_feedback: feedback,
887
+ images: uploadedImages,
888
+ project_directory: currentProjectDir
889
+ }
890
+ });
891
+
892
+ // 重置表单
893
+ feedbackInput.value = '';
894
+ uploadedImages = [];
895
+ imagePreview.innerHTML = '';
896
+ currentRequestId = '';
897
+
898
+ if (countdownInterval) {
899
+ clearInterval(countdownInterval);
900
+ countdownInterval = null;
901
+ }
902
+ }
903
+
904
+ submitBtn.addEventListener('click', submitFeedback);
905
+
906
+ // 快捷键 Ctrl+Enter 提交
907
+ feedbackInput.addEventListener('keydown', (e) => {
908
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
909
+ submitFeedback();
910
+ }
911
+ });
912
+
913
+ // 接收来自插件的消息
914
+ window.addEventListener('message', event => {
915
+ const message = event.data;
916
+
917
+ switch (message.type) {
918
+ case 'showFeedbackRequest':
919
+ waitingStatus.classList.add('hidden');
920
+ feedbackForm.classList.remove('hidden');
921
+
922
+ currentRequestId = message.payload.requestId;
923
+ currentProjectDir = message.payload.projectDir;
924
+ requestTimestamp = message.payload.timestamp;
925
+ requestTimeout = message.payload.timeout;
926
+
927
+ summaryContent.innerHTML = renderMarkdown(message.payload.summary);
928
+ projectInfo.textContent = '📁 ' + message.payload.projectDir;
929
+ feedbackInput.focus();
930
+
931
+ // 启动倒计时
932
+ if (countdownInterval) {
933
+ clearInterval(countdownInterval);
934
+ }
935
+ updateCountdown();
936
+ countdownInterval = setInterval(updateCountdown, 1000);
937
+ break;
938
+
939
+ case 'showWaiting':
940
+ feedbackForm.classList.add('hidden');
941
+ waitingStatus.classList.remove('hidden');
942
+
943
+ if (countdownInterval) {
944
+ clearInterval(countdownInterval);
945
+ countdownInterval = null;
946
+ }
947
+ break;
948
+
949
+ case 'serverStatus':
950
+ if (message.payload.connected) {
951
+ serverStatus.classList.add('connected');
952
+ serverStatusText.textContent = i18n.connected;
953
+ } else {
954
+ serverStatus.classList.remove('connected');
955
+ serverStatusText.textContent = i18n.disconnected;
956
+ }
957
+ break;
958
+ }
959
+ });
960
+
961
+ // 定期检查服务器状态
962
+ setInterval(() => {
963
+ vscode.postMessage({ type: 'checkServer' });
964
+ }, 5000);
965
+
966
+ // 通知插件 WebView 已准备就绪
967
+ vscode.postMessage({ type: 'ready' });
968
+ vscode.postMessage({ type: 'checkServer' });
969
+ </script>
970
+ </body>
971
+ </html>`;
972
+ }
973
+ /**
974
+ * 获取国际化文本
975
+ */
976
+ _getI18n(lang) {
977
+ const translations = {
978
+ 'zh-CN': {
979
+ waiting: '等待 AI 请求反馈...',
980
+ waitingHint: '当 AI 需要您的反馈时,这里会显示输入界面',
981
+ summary: 'AI 工作摘要',
982
+ yourFeedback: '您的反馈',
983
+ placeholder: '请输入您的反馈...',
984
+ continue: '继续',
985
+ confirm: '确认,没问题',
986
+ modify: '请修改',
987
+ cancel: '取消',
988
+ uploadImage: '上传图片',
989
+ submit: '提交反馈',
990
+ timeout: '剩余时间',
991
+ expired: '已超时',
992
+ checking: '检查连接...',
993
+ connected: 'MCP Server 已连接',
994
+ disconnected: 'MCP Server 未连接',
995
+ },
996
+ 'zh-TW': {
997
+ waiting: '等待 AI 請求回饋...',
998
+ waitingHint: '當 AI 需要您的回饋時,這裡會顯示輸入介面',
999
+ summary: 'AI 工作摘要',
1000
+ yourFeedback: '您的回饋',
1001
+ placeholder: '請輸入您的回饋...',
1002
+ continue: '繼續',
1003
+ confirm: '確認,沒問題',
1004
+ modify: '請修改',
1005
+ cancel: '取消',
1006
+ uploadImage: '上傳圖片',
1007
+ submit: '提交回饋',
1008
+ timeout: '剩餘時間',
1009
+ expired: '已超時',
1010
+ checking: '檢查連接...',
1011
+ connected: 'MCP Server 已連接',
1012
+ disconnected: 'MCP Server 未連接',
1013
+ },
1014
+ 'en': {
1015
+ waiting: 'Waiting for AI feedback request...',
1016
+ waitingHint: 'The feedback interface will appear when AI needs your input',
1017
+ summary: 'AI Work Summary',
1018
+ yourFeedback: 'Your Feedback',
1019
+ placeholder: 'Enter your feedback...',
1020
+ continue: 'Continue',
1021
+ confirm: 'Confirm',
1022
+ modify: 'Please modify',
1023
+ cancel: 'Cancel',
1024
+ uploadImage: 'Upload Image',
1025
+ submit: 'Submit Feedback',
1026
+ timeout: 'Time remaining',
1027
+ expired: 'Expired',
1028
+ checking: 'Checking connection...',
1029
+ connected: 'MCP Server connected',
1030
+ disconnected: 'MCP Server disconnected',
1031
+ }
1032
+ };
1033
+ return translations[lang] || translations['zh-CN'];
1034
+ }
1035
+ }
1036
+ FeedbackViewProvider.viewType = 'cursorFeedback.feedbackView';
1037
+ //# sourceMappingURL=extension.js.map