cursor-feedback 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp-server.ts CHANGED
@@ -114,28 +114,32 @@ class McpFeedbackServer {
114
114
  使用规则:
115
115
  1. 每次完成任务或需要与用户沟通时,必须调用此工具
116
116
  2. 收到用户反馈后,必须再次调用此工具继续对话(这是一个持续的对话循环)
117
- 3. 不要直接回复用户,所有回复必须通过此工具
118
- 4. 只有当用户主动提出结束对话时,才能停止调用此工具
117
+ 3. 不要直接回复用户,所有回复必须通过此工具的 summary 参数
118
+ 4. 只有当用户明确提出结束对话时,才能停止调用此工具
119
119
  5. 如果反馈超时,必须立即再次调用此工具继续等待
120
120
  6. 提供清晰的工作摘要,说明你做了什么以及需要什么反馈
121
121
 
122
122
  对话流程:
123
123
  用户消息 → 你工作 → 调用此工具(附带摘要)→ 用户反馈 → 你工作 → 再次调用此工具 → ...
124
124
 
125
+ 返回值处理【必须遵守】:
126
+ - 如果返回包含 "timeout" 或 "cancelled" → 必须立即重新调用此工具继续等待
127
+ - 如果用户明确说"结束/停止/不聊了"等 → 停止调用
128
+ - 其他情况 → 处理用户反馈后再次调用此工具
129
+
125
130
  参数:
126
- project_directory: 项目目录路径(必须是当前工作区的绝对路径)
131
+ project_directory: 【必填】项目目录的绝对路径,用于正确匹配项目窗口
127
132
  summary: AI 工作摘要,供用户查看(支持 Markdown)
128
133
  timeout: 等待用户反馈的超时时间(秒),默认 300 秒(5 分钟)
129
134
 
130
135
  返回:
131
- 用户反馈,包括文字、图片和文件路径`,
136
+ 用户反馈内容(文字/图片/文件路径),或 timeout/cancelled 状态`,
132
137
  inputSchema: {
133
138
  type: 'object',
134
139
  properties: {
135
140
  project_directory: {
136
141
  type: 'string',
137
- description: 'Project directory path for context (MUST be the absolute path of current workspace)',
138
- default: '.',
142
+ description: 'Project directory absolute path (REQUIRED - must be the absolute path of current workspace for correct project matching)',
139
143
  },
140
144
  summary: {
141
145
  type: 'string',
@@ -148,6 +152,7 @@ class McpFeedbackServer {
148
152
  default: 300,
149
153
  },
150
154
  },
155
+ required: ['project_directory'],
151
156
  },
152
157
  },
153
158
  {
@@ -0,0 +1,63 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta http-equiv="Content-Security-Policy" content="{{CSP}}">
7
+ <title>Cursor Feedback</title>
8
+ <script src="{{MARKED_JS_URI}}"></script>
9
+ <link rel="stylesheet" href="{{STYLES_CSS_URI}}">
10
+ </head>
11
+ <body>
12
+ <div class="container">
13
+ <!-- 服务器状态 -->
14
+ <div id="serverStatus" class="server-status">
15
+ <span class="dot"></span>
16
+ <span id="serverStatusText">检查连接...</span>
17
+ <span id="debugIcon" class="debug-icon">🔍</span>
18
+ <div id="debugTooltip" class="debug-tooltip"></div>
19
+ </div>
20
+
21
+ <!-- 等待状态 -->
22
+ <div id="waitingStatus" class="status waiting">
23
+ <div class="status-icon">⏳</div>
24
+ <p>等待 AI 请求反馈...</p>
25
+ <p style="font-size: 11px; margin-top: 10px; opacity: 0.8;">当 AI 需要您的反馈时,这里会显示输入界面</p>
26
+ </div>
27
+
28
+ <!-- 反馈表单 -->
29
+ <div id="feedbackForm" class="hidden">
30
+ <!-- AI 摘要 -->
31
+ <div class="section">
32
+ <div class="section-title">📋 AI 工作摘要</div>
33
+ <div id="summaryContent" class="summary-content"></div>
34
+ <div id="projectInfo" class="project-info"></div>
35
+ </div>
36
+
37
+ <!-- 反馈输入 -->
38
+ <div class="section">
39
+ <div class="section-title">💬 您的反馈</div>
40
+ <textarea id="feedbackInput" class="feedback-input" placeholder="请输入您的反馈..."></textarea>
41
+
42
+ <!-- 附件区域 -->
43
+ <div class="attachments-area">
44
+ <div class="attachment-buttons">
45
+ <button id="uploadBtn" class="attachment-btn" data-tooltip="上传图片">🖼️</button>
46
+ <button id="selectPathBtn" class="attachment-btn" data-tooltip="选择文件/文件夹">📁</button>
47
+ </div>
48
+ <input type="file" id="imageInput" accept="image/*" multiple class="hidden">
49
+ <div id="imagePreview" class="image-preview"></div>
50
+ <div id="fileList" class="file-list"></div>
51
+ </div>
52
+
53
+ <div id="timeoutInfo" class="timeout-info"></div>
54
+ </div>
55
+
56
+ <!-- 提交按钮 -->
57
+ <button id="submitBtn" class="submit-btn">提交反馈 (Ctrl+Enter)</button>
58
+ </div>
59
+ </div>
60
+
61
+ <script src="{{SCRIPT_JS_URI}}"></script>
62
+ </body>
63
+ </html>
@@ -0,0 +1,250 @@
1
+ // WebView 脚本
2
+ (function() {
3
+ const vscode = acquireVsCodeApi();
4
+
5
+ // 恢复之前保存的文本
6
+ const previousState = vscode.getState();
7
+
8
+ // Markdown 渲染
9
+ function renderMarkdown(text) {
10
+ if (!text) return '';
11
+ try {
12
+ if (typeof marked !== 'undefined') {
13
+ marked.setOptions({ breaks: true, gfm: true, headerIds: false });
14
+ return marked.parse(text);
15
+ }
16
+ } catch (e) {
17
+ console.error('Markdown rendering error:', e);
18
+ }
19
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
20
+ }
21
+
22
+ // DOM 元素
23
+ const serverStatus = document.getElementById('serverStatus');
24
+ const serverStatusText = document.getElementById('serverStatusText');
25
+ const debugTooltip = document.getElementById('debugTooltip');
26
+ const waitingStatus = document.getElementById('waitingStatus');
27
+ const feedbackForm = document.getElementById('feedbackForm');
28
+ const summaryContent = document.getElementById('summaryContent');
29
+ const projectInfo = document.getElementById('projectInfo');
30
+ const feedbackInput = document.getElementById('feedbackInput');
31
+ const submitBtn = document.getElementById('submitBtn');
32
+ const uploadBtn = document.getElementById('uploadBtn');
33
+ const selectPathBtn = document.getElementById('selectPathBtn');
34
+ const imageInput = document.getElementById('imageInput');
35
+ const imagePreview = document.getElementById('imagePreview');
36
+ const fileList = document.getElementById('fileList');
37
+ const timeoutInfo = document.getElementById('timeoutInfo');
38
+
39
+ let uploadedImages = [];
40
+ let attachedFiles = [];
41
+ let currentRequestId = '';
42
+ let currentProjectDir = '';
43
+ let requestTimestamp = 0;
44
+ let requestTimeout = 300;
45
+ let countdownInterval = null;
46
+
47
+ // 恢复输入框文本
48
+ if (previousState?.text) {
49
+ feedbackInput.value = previousState.text;
50
+ }
51
+
52
+ // 输入时保存文本
53
+ feedbackInput.addEventListener('input', () => {
54
+ vscode.setState({ text: feedbackInput.value });
55
+ });
56
+
57
+ // 图片上传
58
+ uploadBtn.addEventListener('click', () => imageInput.click());
59
+ selectPathBtn.addEventListener('click', () => vscode.postMessage({ type: 'selectPath' }));
60
+
61
+ // 添加已选文件到列表
62
+ function addAttachedFile(path) {
63
+ if (attachedFiles.includes(path)) return;
64
+ attachedFiles.push(path);
65
+
66
+ const item = document.createElement('div');
67
+ item.className = 'file-item';
68
+
69
+ const icon = document.createElement('span');
70
+ icon.className = 'file-icon';
71
+ icon.textContent = (path.endsWith('/') || !path.split('/').pop().includes('.')) ? '📁' : '📄';
72
+
73
+ const pathSpan = document.createElement('span');
74
+ pathSpan.className = 'file-path';
75
+ pathSpan.textContent = path;
76
+ pathSpan.title = path;
77
+
78
+ const removeBtn = document.createElement('button');
79
+ removeBtn.className = 'file-remove';
80
+ removeBtn.textContent = '×';
81
+ removeBtn.onclick = () => {
82
+ const idx = attachedFiles.indexOf(path);
83
+ if (idx > -1) attachedFiles.splice(idx, 1);
84
+ item.remove();
85
+ };
86
+
87
+ item.appendChild(icon);
88
+ item.appendChild(pathSpan);
89
+ item.appendChild(removeBtn);
90
+ fileList.appendChild(item);
91
+ }
92
+
93
+ imageInput.addEventListener('change', (e) => {
94
+ for (const file of e.target.files) addImageFile(file);
95
+ });
96
+
97
+ // 添加图片到预览
98
+ function addImageFile(file) {
99
+ const reader = new FileReader();
100
+ reader.onload = (e) => {
101
+ const base64 = e.target.result;
102
+ const imgData = {
103
+ name: file.name || ('pasted-image-' + Date.now() + '.png'),
104
+ data: base64.split(',')[1],
105
+ size: file.size
106
+ };
107
+ uploadedImages.push(imgData);
108
+
109
+ const container = document.createElement('div');
110
+ container.className = 'image-preview-item';
111
+
112
+ const img = document.createElement('img');
113
+ img.src = base64;
114
+
115
+ const removeBtn = document.createElement('button');
116
+ removeBtn.className = 'image-remove';
117
+ removeBtn.textContent = '×';
118
+ removeBtn.onclick = () => {
119
+ const index = uploadedImages.indexOf(imgData);
120
+ if (index > -1) uploadedImages.splice(index, 1);
121
+ container.remove();
122
+ };
123
+
124
+ container.appendChild(img);
125
+ container.appendChild(removeBtn);
126
+ imagePreview.appendChild(container);
127
+ };
128
+ reader.readAsDataURL(file);
129
+ }
130
+
131
+ // 粘贴图片支持
132
+ document.addEventListener('paste', (e) => {
133
+ const items = e.clipboardData?.items;
134
+ if (!items) return;
135
+ for (const item of items) {
136
+ if (item.type.startsWith('image/')) {
137
+ e.preventDefault();
138
+ const file = item.getAsFile();
139
+ if (file) addImageFile(file);
140
+ }
141
+ }
142
+ });
143
+
144
+ // 更新倒计时
145
+ function updateCountdown() {
146
+ if (!requestTimestamp || !requestTimeout) return;
147
+ const elapsed = Math.floor((Date.now() - requestTimestamp) / 1000);
148
+ const remaining = Math.max(0, requestTimeout - elapsed);
149
+ const minutes = Math.floor(remaining / 60);
150
+ const seconds = remaining % 60;
151
+ timeoutInfo.textContent = '剩余时间: ' + minutes + ':' + seconds.toString().padStart(2, '0');
152
+ if (remaining <= 0) {
153
+ clearInterval(countdownInterval);
154
+ timeoutInfo.textContent = '已超时';
155
+ }
156
+ }
157
+
158
+ // 提交反馈
159
+ function submitFeedback() {
160
+ if (!currentRequestId) return;
161
+
162
+ vscode.postMessage({
163
+ type: 'submitFeedback',
164
+ payload: {
165
+ requestId: currentRequestId,
166
+ interactive_feedback: feedbackInput.value.trim(),
167
+ images: uploadedImages,
168
+ attachedFiles: attachedFiles,
169
+ project_directory: currentProjectDir
170
+ }
171
+ });
172
+
173
+ // 重置表单
174
+ feedbackInput.value = '';
175
+ uploadedImages = [];
176
+ attachedFiles = [];
177
+ imagePreview.innerHTML = '';
178
+ fileList.innerHTML = '';
179
+ currentRequestId = '';
180
+ vscode.setState({}); // 清除保存的文本
181
+
182
+ if (countdownInterval) {
183
+ clearInterval(countdownInterval);
184
+ countdownInterval = null;
185
+ }
186
+ }
187
+
188
+ submitBtn.addEventListener('click', submitFeedback);
189
+ feedbackInput.addEventListener('keydown', (e) => {
190
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') submitFeedback();
191
+ });
192
+
193
+ // 接收消息
194
+ window.addEventListener('message', event => {
195
+ const message = event.data;
196
+
197
+ switch (message.type) {
198
+ case 'showFeedbackRequest':
199
+ waitingStatus.classList.add('hidden');
200
+ feedbackForm.classList.remove('hidden');
201
+ currentRequestId = message.payload.requestId;
202
+ currentProjectDir = message.payload.projectDir;
203
+ requestTimestamp = message.payload.timestamp;
204
+ requestTimeout = message.payload.timeout;
205
+ summaryContent.innerHTML = renderMarkdown(message.payload.summary);
206
+ summaryContent.scrollTop = 0;
207
+ projectInfo.textContent = '📁 ' + message.payload.projectDir;
208
+ feedbackInput.focus();
209
+ if (countdownInterval) clearInterval(countdownInterval);
210
+ updateCountdown();
211
+ countdownInterval = setInterval(updateCountdown, 1000);
212
+ break;
213
+
214
+ case 'showWaiting':
215
+ feedbackForm.classList.add('hidden');
216
+ waitingStatus.classList.remove('hidden');
217
+ if (countdownInterval) {
218
+ clearInterval(countdownInterval);
219
+ countdownInterval = null;
220
+ }
221
+ break;
222
+
223
+ case 'serverStatus':
224
+ if (message.payload.connected) {
225
+ serverStatus.classList.add('connected');
226
+ serverStatusText.textContent = 'MCP Server 已连接';
227
+ } else {
228
+ serverStatus.classList.remove('connected');
229
+ serverStatusText.textContent = 'MCP Server 未连接';
230
+ }
231
+ break;
232
+
233
+ case 'updateDebugInfo':
234
+ const d = message.payload;
235
+ debugTooltip.textContent = `🔍 调试信息\n━━━━━━━━━━━━\n扫描端口: ${d.portRange}\n工作区: ${d.workspacePath}\n当前端口: ${d.activePort || '-'}\n已连接: ${d.connectedPorts.length > 0 ? d.connectedPorts.join(', ') : '无'}\n状态: ${d.lastStatus}`;
236
+ break;
237
+
238
+ case 'filesSelected':
239
+ if (message.payload.paths) {
240
+ for (const path of message.payload.paths) addAttachedFile(path);
241
+ }
242
+ break;
243
+ }
244
+ });
245
+
246
+ // 定期检查服务器状态
247
+ setInterval(() => vscode.postMessage({ type: 'checkServer' }), 5000);
248
+ vscode.postMessage({ type: 'ready' });
249
+ vscode.postMessage({ type: 'checkServer' });
250
+ })();