cursor-feedback 1.1.1 → 2.0.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.
- package/CHANGELOG.md +14 -0
- package/README.md +48 -4
- package/README_CN.md +48 -4
- package/dist/extension.js +546 -53
- package/dist/feishu.js +667 -0
- package/dist/i18n/en.json +49 -7
- package/dist/i18n/index.js +46 -4
- package/dist/i18n/zh-CN.json +49 -7
- package/dist/mcp-server.js +514 -20
- package/dist/webview/index.html +193 -43
- package/dist/webview/script.js +795 -145
- package/dist/webview/styles.css +921 -298
- package/package.json +38 -1
- package/.github/workflows/release-please.yml +0 -19
- package/.husky/pre-commit +0 -2
- package/.husky/pre-push +0 -1
- package/.versionrc.json +0 -15
- package/.vscode/launch.json +0 -28
- package/.vscode/tasks.json +0 -22
- package/.vscodeignore +0 -11
- package/demo.gif +0 -0
- package/dist/extension.js.map +0 -1
- package/dist/i18n/index.js.map +0 -1
- package/dist/mcp-server.js.map +0 -1
- package/icon.png +0 -0
- package/mcp-install-dark.png +0 -0
- package/resources/icon.svg +0 -4
- package/resources/vendor/marked.min.js +0 -69
- package/scripts/check-changelog.js +0 -42
- package/src/extension.ts +0 -692
- package/src/i18n/en.json +0 -34
- package/src/i18n/index.ts +0 -131
- package/src/i18n/zh-CN.json +0 -34
- package/src/mcp-server.ts +0 -768
- package/src/webview/index.html +0 -69
- package/src/webview/script.js +0 -318
- package/src/webview/styles.css +0 -422
- package/tsconfig.json +0 -17
package/dist/webview/script.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
// WebView 脚本
|
|
2
|
-
(function() {
|
|
2
|
+
(function () {
|
|
3
3
|
const vscode = acquireVsCodeApi();
|
|
4
4
|
const i18n = window.i18n || {};
|
|
5
5
|
|
|
6
|
-
// 恢复之前保存的文本
|
|
7
6
|
const previousState = vscode.getState();
|
|
8
7
|
|
|
9
|
-
//
|
|
8
|
+
// ---------- SVG icon helpers ----------
|
|
9
|
+
const ICONS = {
|
|
10
|
+
remove: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
11
|
+
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
|
12
|
+
folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
|
13
|
+
code: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>'
|
|
14
|
+
};
|
|
15
|
+
function svg(paths, cls) {
|
|
16
|
+
const el = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
17
|
+
el.setAttribute('viewBox', '0 0 24 24');
|
|
18
|
+
el.setAttribute('fill', 'none');
|
|
19
|
+
el.setAttribute('stroke', 'currentColor');
|
|
20
|
+
el.setAttribute('stroke-width', '2');
|
|
21
|
+
el.setAttribute('stroke-linecap', 'round');
|
|
22
|
+
el.setAttribute('stroke-linejoin', 'round');
|
|
23
|
+
el.setAttribute('aria-hidden', 'true');
|
|
24
|
+
if (cls) el.setAttribute('class', cls);
|
|
25
|
+
el.innerHTML = paths;
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------- Markdown ----------
|
|
10
30
|
function renderMarkdown(text) {
|
|
11
31
|
if (!text) return '';
|
|
12
32
|
try {
|
|
@@ -20,7 +40,15 @@
|
|
|
20
40
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
|
21
41
|
}
|
|
22
42
|
|
|
23
|
-
//
|
|
43
|
+
// 代码块语法高亮(highlight.js):markdown 渲染后对 pre code 上色
|
|
44
|
+
function highlightCodeBlocks(container) {
|
|
45
|
+
if (typeof hljs === 'undefined' || !container) return;
|
|
46
|
+
container.querySelectorAll('pre code').forEach((el) => {
|
|
47
|
+
try { hljs.highlightElement(el); } catch (e) { /* ignore */ }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------- DOM ----------
|
|
24
52
|
const serverStatus = document.getElementById('serverStatus');
|
|
25
53
|
const serverStatusText = document.getElementById('serverStatusText');
|
|
26
54
|
const debugTooltip = document.getElementById('debugTooltip');
|
|
@@ -31,149 +59,563 @@
|
|
|
31
59
|
const projectInfo = document.getElementById('projectInfo');
|
|
32
60
|
const feedbackInput = document.getElementById('feedbackInput');
|
|
33
61
|
const submitBtn = document.getElementById('submitBtn');
|
|
62
|
+
const submitLabel = document.getElementById('submitLabel');
|
|
63
|
+
const submitKbd = document.getElementById('submitKbd');
|
|
34
64
|
const uploadBtn = document.getElementById('uploadBtn');
|
|
35
65
|
const selectPathBtn = document.getElementById('selectPathBtn');
|
|
66
|
+
const insertContextBtn = document.getElementById('insertContextBtn');
|
|
67
|
+
const mentionPopup = document.getElementById('mentionPopup');
|
|
36
68
|
const imageInput = document.getElementById('imageInput');
|
|
37
69
|
const imagePreview = document.getElementById('imagePreview');
|
|
38
|
-
const
|
|
70
|
+
const refChips = document.getElementById('refChips');
|
|
71
|
+
const charCount = document.getElementById('charCount');
|
|
72
|
+
const timeoutWrap = document.getElementById('timeoutWrap');
|
|
73
|
+
const timeoutBar = document.getElementById('timeoutBar');
|
|
39
74
|
const timeoutInfo = document.getElementById('timeoutInfo');
|
|
40
75
|
const toggleKeyModeBtn = document.getElementById('toggleKeyModeBtn');
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
76
|
+
const dropOverlay = document.getElementById('dropOverlay');
|
|
77
|
+
const summaryCard = document.getElementById('summaryCard');
|
|
78
|
+
const splitter = document.getElementById('splitter');
|
|
79
|
+
const autoRetryBtn = document.getElementById('autoRetryBtn');
|
|
80
|
+
const themeAccentBtn = document.getElementById('themeAccentBtn');
|
|
81
|
+
const submitBar = document.getElementById('submitBar');
|
|
82
|
+
const lightbox = document.getElementById('lightbox');
|
|
83
|
+
const lightboxImg = document.getElementById('lightboxImg');
|
|
84
|
+
const feishuBtn = document.getElementById('feishuBtn');
|
|
85
|
+
const feishuModal = document.getElementById('feishuModal');
|
|
86
|
+
const feishuCloseBtn = document.getElementById('feishuCloseBtn');
|
|
87
|
+
const feishuAppIdInput = document.getElementById('feishuAppId');
|
|
88
|
+
const feishuAppSecretInput = document.getElementById('feishuAppSecret');
|
|
89
|
+
const feishuStatusEl = document.querySelector('.feishu-status');
|
|
90
|
+
const feishuStatusText = document.getElementById('feishuStatusText');
|
|
91
|
+
const feishuGuideBtn = document.getElementById('feishuGuideBtn');
|
|
92
|
+
const feishuSecretToggle = document.getElementById('feishuSecretToggle');
|
|
93
|
+
const feishuEnabledToggle = document.getElementById('feishuEnabledToggle');
|
|
94
|
+
const systemNotifyToggle = document.getElementById('systemNotifyToggle');
|
|
95
|
+
const osNotifyToggle = document.getElementById('osNotifyToggle');
|
|
96
|
+
const feishuAckToggle = document.getElementById('feishuAckToggle');
|
|
97
|
+
const osNotifySub = document.getElementById('osNotifySub');
|
|
98
|
+
const feishuAckSub = document.getElementById('feishuAckSub');
|
|
46
99
|
|
|
47
100
|
let uploadedImages = [];
|
|
48
101
|
let attachedFiles = [];
|
|
102
|
+
let codeRefs = [];
|
|
49
103
|
let currentRequestId = '';
|
|
50
104
|
let currentProjectDir = '';
|
|
51
105
|
let requestTimestamp = 0;
|
|
52
106
|
let requestTimeout = 300;
|
|
53
107
|
let countdownInterval = null;
|
|
108
|
+
let submitFallbackTimer = null;
|
|
109
|
+
|
|
110
|
+
// ---------- Language switch ----------
|
|
111
|
+
langSwitchBtn.addEventListener('click', () => {
|
|
112
|
+
vscode.postMessage({ type: 'switchLanguage' });
|
|
113
|
+
});
|
|
54
114
|
|
|
55
|
-
//
|
|
115
|
+
// ---------- Timeout keep-waiting toggle ----------
|
|
116
|
+
autoRetryBtn.addEventListener('click', () => {
|
|
117
|
+
vscode.postMessage({ type: 'toggleAutoRetry' });
|
|
118
|
+
});
|
|
119
|
+
function updateAutoRetryUI(enabled) {
|
|
120
|
+
autoRetryBtn.classList.toggle('is-on', enabled);
|
|
121
|
+
autoRetryBtn.classList.toggle('is-off', !enabled);
|
|
122
|
+
const tip = enabled
|
|
123
|
+
? (i18n.autoRetryOn || 'Timeout: keep waiting (click to end on timeout)')
|
|
124
|
+
: (i18n.autoRetryOff || 'Timeout: end turn (click to keep waiting)');
|
|
125
|
+
autoRetryBtn.setAttribute('data-tip', tip);
|
|
126
|
+
autoRetryBtn.setAttribute('aria-label', tip);
|
|
127
|
+
}
|
|
128
|
+
updateAutoRetryUI(true);
|
|
129
|
+
|
|
130
|
+
// ---------- Accent theme toggle (brand pink ⇄ follow IDE) ----------
|
|
131
|
+
const ACCENT_KEY = 'cursorFeedback_accentMode';
|
|
132
|
+
let accentMode = localStorage.getItem(ACCENT_KEY) === 'ide' ? 'ide' : 'brand';
|
|
133
|
+
function applyAccentMode(mode) {
|
|
134
|
+
accentMode = mode === 'ide' ? 'ide' : 'brand';
|
|
135
|
+
document.body.classList.toggle('accent-ide', accentMode === 'ide');
|
|
136
|
+
const isBrand = accentMode === 'brand';
|
|
137
|
+
themeAccentBtn.classList.toggle('is-on', isBrand);
|
|
138
|
+
themeAccentBtn.classList.toggle('is-off', !isBrand);
|
|
139
|
+
const tip = isBrand
|
|
140
|
+
? (i18n.accentBrand || 'Accent: brand pink (click to follow IDE)')
|
|
141
|
+
: (i18n.accentIde || 'Accent: follow IDE color (click for brand pink)');
|
|
142
|
+
themeAccentBtn.setAttribute('data-tip', tip);
|
|
143
|
+
themeAccentBtn.setAttribute('aria-label', tip);
|
|
144
|
+
}
|
|
145
|
+
themeAccentBtn.addEventListener('click', () => {
|
|
146
|
+
applyAccentMode(accentMode === 'brand' ? 'ide' : 'brand');
|
|
147
|
+
localStorage.setItem(ACCENT_KEY, accentMode);
|
|
148
|
+
});
|
|
149
|
+
applyAccentMode(accentMode);
|
|
150
|
+
|
|
151
|
+
// ---------- Feishu notification settings ----------
|
|
152
|
+
let feishuLastSavedSig = null;
|
|
153
|
+
let feishuLastState = null;
|
|
154
|
+
// 失焦即保存(已去掉「保存」按钮):提交输入框当前值,与原保存按钮等价。
|
|
155
|
+
// 同一 appId 下 secret 留空时后端会保留旧密钥,故自动保存安全;用 sig 去重避免无谓重存。
|
|
156
|
+
function saveFeishuConfigFromInputs() {
|
|
157
|
+
const appId = feishuAppIdInput.value.trim();
|
|
158
|
+
const appSecret = feishuAppSecretInput.value.trim();
|
|
159
|
+
const sig = appId + '\u0000' + appSecret;
|
|
160
|
+
// 与上次提交值一致就不重复提交(基线为 null 时视为空,避免无谓提交)。
|
|
161
|
+
if (sig === (feishuLastSavedSig ?? '\u0000')) return;
|
|
162
|
+
feishuLastSavedSig = sig;
|
|
163
|
+
vscode.postMessage({ type: 'saveFeishuConfig', payload: { appId, appSecret } });
|
|
164
|
+
}
|
|
165
|
+
feishuAppIdInput.addEventListener('blur', () => saveFeishuConfigFromInputs());
|
|
166
|
+
feishuAppSecretInput.addEventListener('blur', () => saveFeishuConfigFromInputs());
|
|
167
|
+
|
|
168
|
+
// 打开弹窗时读一次 server 当前凭证填入;之后后台轮询不再碰输入框(避免编辑时被覆盖/清空)。
|
|
169
|
+
function fillFeishuInputs() {
|
|
170
|
+
const s = feishuLastState;
|
|
171
|
+
if (!s) return;
|
|
172
|
+
if (typeof s.appId === 'string') feishuAppIdInput.value = s.appId;
|
|
173
|
+
if (typeof s.appSecret === 'string') feishuAppSecretInput.value = s.appSecret;
|
|
174
|
+
feishuLastSavedSig = feishuAppIdInput.value.trim() + '\u0000' + feishuAppSecretInput.value.trim();
|
|
175
|
+
}
|
|
176
|
+
function openFeishuModal() {
|
|
177
|
+
fillFeishuInputs();
|
|
178
|
+
feishuModal.classList.remove('hidden');
|
|
179
|
+
feishuModal.setAttribute('aria-hidden', 'false');
|
|
180
|
+
setTimeout(() => feishuAppIdInput.focus(), 0);
|
|
181
|
+
}
|
|
182
|
+
function closeFeishuModal() {
|
|
183
|
+
// 兜底:Esc 关闭等场景输入框可能不触发 blur,关闭前再存一次(已去重,不会重复提交)
|
|
184
|
+
saveFeishuConfigFromInputs();
|
|
185
|
+
feishuModal.classList.add('hidden');
|
|
186
|
+
feishuModal.setAttribute('aria-hidden', 'true');
|
|
187
|
+
}
|
|
188
|
+
feishuBtn.addEventListener('click', openFeishuModal);
|
|
189
|
+
feishuCloseBtn.addEventListener('click', closeFeishuModal);
|
|
190
|
+
feishuModal.addEventListener('click', (e) => {
|
|
191
|
+
if (e.target === feishuModal) closeFeishuModal();
|
|
192
|
+
});
|
|
193
|
+
document.addEventListener('keydown', (e) => {
|
|
194
|
+
if (e.key === 'Escape' && !feishuModal.classList.contains('hidden')) {
|
|
195
|
+
e.stopPropagation();
|
|
196
|
+
closeFeishuModal();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
feishuGuideBtn.addEventListener('click', () => {
|
|
200
|
+
vscode.postMessage({ type: 'openFeishuGuide' });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 密钥显示/隐藏切换(小眼睛)
|
|
204
|
+
feishuSecretToggle.addEventListener('click', () => {
|
|
205
|
+
const show = feishuAppSecretInput.type === 'password';
|
|
206
|
+
feishuAppSecretInput.type = show ? 'text' : 'password';
|
|
207
|
+
feishuSecretToggle.classList.toggle('is-visible', show);
|
|
208
|
+
const tip = show ? (i18n.hideSecret || 'Hide secret') : (i18n.showSecret || 'Show secret');
|
|
209
|
+
feishuSecretToggle.setAttribute('aria-label', tip);
|
|
210
|
+
feishuSecretToggle.setAttribute('data-tip', tip);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// 通知开关(主:飞书通知 / 插件通知;子:Get 表情回执 / 失焦系统提示)
|
|
214
|
+
feishuEnabledToggle.addEventListener('change', () => {
|
|
215
|
+
vscode.postMessage({ type: 'toggleFeishuEnabled', payload: { enabled: feishuEnabledToggle.checked } });
|
|
216
|
+
syncSubSwitchDisabled();
|
|
217
|
+
});
|
|
218
|
+
systemNotifyToggle.addEventListener('change', () => {
|
|
219
|
+
vscode.postMessage({ type: 'toggleSystemNotification', payload: { enabled: systemNotifyToggle.checked } });
|
|
220
|
+
syncSubSwitchDisabled();
|
|
221
|
+
});
|
|
222
|
+
osNotifyToggle.addEventListener('change', () => {
|
|
223
|
+
vscode.postMessage({ type: 'toggleOsNotification', payload: { enabled: osNotifyToggle.checked } });
|
|
224
|
+
});
|
|
225
|
+
feishuAckToggle.addEventListener('change', () => {
|
|
226
|
+
vscode.postMessage({ type: 'toggleFeishuAck', payload: { enabled: feishuAckToggle.checked } });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// 子开关受主开关约束:主关时子置灰禁用
|
|
230
|
+
function syncSubSwitchDisabled() {
|
|
231
|
+
const osOn = systemNotifyToggle.checked;
|
|
232
|
+
osNotifyToggle.disabled = !osOn;
|
|
233
|
+
if (osNotifySub) osNotifySub.classList.toggle('is-disabled', !osOn);
|
|
234
|
+
const feishuOn = feishuEnabledToggle.checked;
|
|
235
|
+
feishuAckToggle.disabled = !feishuOn;
|
|
236
|
+
if (feishuAckSub) feishuAckSub.classList.toggle('is-disabled', !feishuOn);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function updateFeishuUI(state) {
|
|
240
|
+
if (!state) return;
|
|
241
|
+
// 凭证输入框不在这里刷新:后台轮询只缓存最新值,由打开弹窗时 fillFeishuInputs() 填入一次,
|
|
242
|
+
// 避免编辑过程中被后端回显覆盖/清空。绿点、开关等非输入框状态照常实时刷新。
|
|
243
|
+
feishuLastState = state;
|
|
244
|
+
if (typeof state.feishuEnabled === 'boolean') {
|
|
245
|
+
feishuEnabledToggle.checked = state.feishuEnabled;
|
|
246
|
+
}
|
|
247
|
+
if (typeof state.systemNotification === 'boolean') {
|
|
248
|
+
systemNotifyToggle.checked = state.systemNotification;
|
|
249
|
+
}
|
|
250
|
+
if (typeof state.osNotification === 'boolean') {
|
|
251
|
+
osNotifyToggle.checked = state.osNotification;
|
|
252
|
+
}
|
|
253
|
+
if (typeof state.feishuAck === 'boolean') {
|
|
254
|
+
feishuAckToggle.checked = state.feishuAck;
|
|
255
|
+
}
|
|
256
|
+
syncSubSwitchDisabled();
|
|
257
|
+
feishuStatusEl.classList.toggle('is-configured', !!state.configured && !state.bound);
|
|
258
|
+
feishuStatusEl.classList.toggle('is-bound', !!state.bound);
|
|
259
|
+
let txt = i18n.feishuStatusUnconfigured || 'Not configured';
|
|
260
|
+
if (state.bound) txt = i18n.feishuStatusBound || 'Configured & bound';
|
|
261
|
+
else if (state.configured) txt = i18n.feishuStatusConfigured || 'Configured';
|
|
262
|
+
feishuStatusText.textContent = txt;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------- Lightbox (image zoom) ----------
|
|
266
|
+
function openLightbox(src) {
|
|
267
|
+
lightboxImg.src = src;
|
|
268
|
+
lightbox.classList.remove('hidden');
|
|
269
|
+
}
|
|
270
|
+
function closeLightbox() {
|
|
271
|
+
lightbox.classList.add('hidden');
|
|
272
|
+
lightboxImg.src = '';
|
|
273
|
+
}
|
|
274
|
+
lightbox.addEventListener('click', closeLightbox);
|
|
275
|
+
document.addEventListener('keydown', (e) => {
|
|
276
|
+
if (e.key === 'Escape' && !lightbox.classList.contains('hidden')) closeLightbox();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ---------- Key mode (Enter vs Ctrl+Enter) ----------
|
|
56
280
|
let enterToSubmit = localStorage.getItem('cursorFeedback_enterToSubmit') === 'true';
|
|
57
281
|
|
|
58
|
-
// 更新快捷键模式 UI
|
|
59
282
|
function updateKeyModeUI() {
|
|
283
|
+
submitLabel.textContent = i18n.submitFeedback || 'Submit';
|
|
60
284
|
if (enterToSubmit) {
|
|
61
|
-
|
|
285
|
+
if (submitKbd) submitKbd.textContent = i18n.submitHintEnter || 'Enter';
|
|
62
286
|
toggleKeyModeBtn.classList.add('enter-mode');
|
|
63
287
|
toggleKeyModeBtn.title = i18n.switchToCtrlEnter || 'Click to switch to Ctrl+Enter submit';
|
|
64
288
|
} else {
|
|
65
|
-
|
|
289
|
+
if (submitKbd) submitKbd.textContent = i18n.submitHintCtrl || 'Ctrl+Enter';
|
|
66
290
|
toggleKeyModeBtn.classList.remove('enter-mode');
|
|
67
291
|
toggleKeyModeBtn.title = i18n.switchToEnter || 'Click to switch to Enter submit';
|
|
68
292
|
}
|
|
69
293
|
}
|
|
70
|
-
|
|
71
|
-
// 初始化快捷键模式 UI
|
|
72
294
|
updateKeyModeUI();
|
|
73
295
|
|
|
74
|
-
// 切换快捷键模式
|
|
75
296
|
toggleKeyModeBtn.addEventListener('click', () => {
|
|
76
297
|
enterToSubmit = !enterToSubmit;
|
|
77
298
|
localStorage.setItem('cursorFeedback_enterToSubmit', enterToSubmit.toString());
|
|
78
299
|
updateKeyModeUI();
|
|
79
300
|
});
|
|
80
301
|
|
|
81
|
-
//
|
|
302
|
+
// ---------- IME composition ----------
|
|
82
303
|
let isComposing = false;
|
|
83
|
-
feedbackInput.addEventListener('compositionstart', () => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
304
|
+
feedbackInput.addEventListener('compositionstart', () => { isComposing = true; });
|
|
305
|
+
feedbackInput.addEventListener('compositionend', () => { isComposing = false; });
|
|
306
|
+
|
|
307
|
+
// ---------- Draft persistence (text + images + files) ----------
|
|
308
|
+
// 切换侧边栏/隐藏 webview 会销毁重建,靠 vscode state 把草稿(含图片、附件)整体存下来
|
|
309
|
+
function saveDraft() {
|
|
310
|
+
vscode.setState({
|
|
311
|
+
text: feedbackInput.value,
|
|
312
|
+
images: uploadedImages,
|
|
313
|
+
files: attachedFiles,
|
|
314
|
+
codeRefs: codeRefs
|
|
315
|
+
});
|
|
316
|
+
}
|
|
89
317
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
feedbackInput.value = previousState.text;
|
|
318
|
+
function updateCharCount() {
|
|
319
|
+
charCount.textContent = String(feedbackInput.value.length);
|
|
93
320
|
}
|
|
94
321
|
|
|
95
|
-
// 输入时保存文本
|
|
96
322
|
feedbackInput.addEventListener('input', () => {
|
|
97
|
-
|
|
323
|
+
saveDraft();
|
|
324
|
+
updateCharCount();
|
|
325
|
+
checkMention();
|
|
98
326
|
});
|
|
99
327
|
|
|
100
|
-
//
|
|
328
|
+
// ---------- Attachments: pickers ----------
|
|
101
329
|
uploadBtn.addEventListener('click', () => imageInput.click());
|
|
102
330
|
selectPathBtn.addEventListener('click', () => vscode.postMessage({ type: 'selectPath' }));
|
|
103
331
|
|
|
104
|
-
|
|
332
|
+
function makeRemoveBtn(onClick, extraClass) {
|
|
333
|
+
const btn = document.createElement('button');
|
|
334
|
+
btn.type = 'button';
|
|
335
|
+
btn.className = 'chip-remove' + (extraClass ? ' ' + extraClass : '');
|
|
336
|
+
btn.setAttribute('aria-label', i18n.removeAttachment || 'Remove');
|
|
337
|
+
btn.appendChild(svg(ICONS.remove));
|
|
338
|
+
btn.addEventListener('click', onClick);
|
|
339
|
+
return btn;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function isDirPath(p) {
|
|
343
|
+
const last = p.replace(/[/\\]+$/, '').split(/[/\\]/).pop() || '';
|
|
344
|
+
return p.endsWith('/') || p.endsWith('\\') || !last.includes('.');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function basename(p) {
|
|
348
|
+
return (p || '').replace(/[/\\]+$/, '').split(/[/\\]/).pop() || p;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function makeRefChip(opts) {
|
|
352
|
+
const chip = document.createElement('div');
|
|
353
|
+
chip.className = 'ref-chip';
|
|
354
|
+
chip.title = opts.title || opts.name;
|
|
355
|
+
|
|
356
|
+
const ic = document.createElement('span');
|
|
357
|
+
ic.className = 'ref-chip__icon';
|
|
358
|
+
ic.appendChild(svg(opts.icon));
|
|
359
|
+
chip.appendChild(ic);
|
|
360
|
+
|
|
361
|
+
const nm = document.createElement('span');
|
|
362
|
+
nm.className = 'ref-chip__name';
|
|
363
|
+
nm.textContent = opts.name;
|
|
364
|
+
chip.appendChild(nm);
|
|
365
|
+
|
|
366
|
+
if (opts.meta) {
|
|
367
|
+
const mt = document.createElement('span');
|
|
368
|
+
mt.className = 'ref-chip__meta';
|
|
369
|
+
mt.textContent = opts.meta;
|
|
370
|
+
chip.appendChild(mt);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
chip.appendChild(makeRemoveBtn(opts.onRemove));
|
|
374
|
+
return chip;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 统一渲染输入框上方的引用 chip:文件引用 + 代码引用(仿 Cursor)
|
|
378
|
+
function renderRefChips() {
|
|
379
|
+
refChips.innerHTML = '';
|
|
380
|
+
attachedFiles.forEach((path) => {
|
|
381
|
+
refChips.appendChild(makeRefChip({
|
|
382
|
+
icon: isDirPath(path) ? ICONS.folder : ICONS.file,
|
|
383
|
+
name: basename(path),
|
|
384
|
+
title: path,
|
|
385
|
+
onRemove: () => {
|
|
386
|
+
const idx = attachedFiles.indexOf(path);
|
|
387
|
+
if (idx > -1) attachedFiles.splice(idx, 1);
|
|
388
|
+
renderRefChips();
|
|
389
|
+
saveDraft();
|
|
390
|
+
}
|
|
391
|
+
}));
|
|
392
|
+
});
|
|
393
|
+
codeRefs.forEach((ref) => {
|
|
394
|
+
const lines = ref.startLine === ref.endLine ? String(ref.startLine) : (ref.startLine + '-' + ref.endLine);
|
|
395
|
+
refChips.appendChild(makeRefChip({
|
|
396
|
+
icon: ICONS.code,
|
|
397
|
+
name: ref.fileName,
|
|
398
|
+
meta: ':' + lines,
|
|
399
|
+
title: ref.fileName + ':' + lines,
|
|
400
|
+
onRemove: () => {
|
|
401
|
+
const idx = codeRefs.indexOf(ref);
|
|
402
|
+
if (idx > -1) codeRefs.splice(idx, 1);
|
|
403
|
+
renderRefChips();
|
|
404
|
+
saveDraft();
|
|
405
|
+
}
|
|
406
|
+
}));
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
105
410
|
function addAttachedFile(path) {
|
|
106
411
|
if (attachedFiles.includes(path)) return;
|
|
107
412
|
attachedFiles.push(path);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
item.className = 'file-item';
|
|
111
|
-
|
|
112
|
-
const icon = document.createElement('span');
|
|
113
|
-
icon.className = 'file-icon';
|
|
114
|
-
icon.textContent = (path.endsWith('/') || !path.split('/').pop().includes('.')) ? '📁' : '📄';
|
|
115
|
-
|
|
116
|
-
const pathSpan = document.createElement('span');
|
|
117
|
-
pathSpan.className = 'file-path';
|
|
118
|
-
pathSpan.textContent = path;
|
|
119
|
-
pathSpan.title = path;
|
|
120
|
-
|
|
121
|
-
const removeBtn = document.createElement('button');
|
|
122
|
-
removeBtn.className = 'file-remove';
|
|
123
|
-
removeBtn.textContent = '×';
|
|
124
|
-
removeBtn.onclick = () => {
|
|
125
|
-
const idx = attachedFiles.indexOf(path);
|
|
126
|
-
if (idx > -1) attachedFiles.splice(idx, 1);
|
|
127
|
-
item.remove();
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
item.appendChild(icon);
|
|
131
|
-
item.appendChild(pathSpan);
|
|
132
|
-
item.appendChild(removeBtn);
|
|
133
|
-
fileList.appendChild(item);
|
|
413
|
+
renderRefChips();
|
|
414
|
+
saveDraft();
|
|
134
415
|
}
|
|
135
416
|
|
|
136
|
-
|
|
137
|
-
|
|
417
|
+
// ---------- @ mention 文件选择器 ----------
|
|
418
|
+
let allFilesCache = null;
|
|
419
|
+
let allFilesCacheTime = 0;
|
|
420
|
+
const FILES_CACHE_TTL = 30000; // 30s 后失效,让会话中新建的文件也能被 @ 到
|
|
421
|
+
let mentionItems = [];
|
|
422
|
+
let mentionIndex = 0;
|
|
423
|
+
let mentionStart = -1;
|
|
424
|
+
let mentionOpen = false;
|
|
425
|
+
let pendingMentionQuery = null;
|
|
426
|
+
let mentionDismissedAt = -1; // 被 Esc 主动取消的 @ 起点,避免同一个 @ 继续打字又弹出
|
|
427
|
+
|
|
428
|
+
insertContextBtn.addEventListener('click', () => vscode.postMessage({ type: 'requestSelection' }));
|
|
429
|
+
|
|
430
|
+
function checkMention() {
|
|
431
|
+
const pos = feedbackInput.selectionStart;
|
|
432
|
+
const before = feedbackInput.value.slice(0, pos);
|
|
433
|
+
const m = before.match(/@([^\s@]*)$/);
|
|
434
|
+
if (!m) { closeMention(); mentionDismissedAt = -1; return; }
|
|
435
|
+
const start = pos - m[0].length;
|
|
436
|
+
if (start === mentionDismissedAt) { return; } // 该 @ 已被 Esc 取消,继续打字不再自动弹
|
|
437
|
+
mentionStart = start;
|
|
438
|
+
const query = m[1];
|
|
439
|
+
const cacheFresh = allFilesCache && (Date.now() - allFilesCacheTime < FILES_CACHE_TTL);
|
|
440
|
+
if (!cacheFresh) {
|
|
441
|
+
pendingMentionQuery = query;
|
|
442
|
+
vscode.postMessage({ type: 'searchFiles' });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
renderMention(query);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function renderMention(query) {
|
|
449
|
+
const q = (query || '').toLowerCase();
|
|
450
|
+
mentionItems = (allFilesCache || [])
|
|
451
|
+
.filter(f => f.rel.toLowerCase().includes(q))
|
|
452
|
+
.slice(0, 50);
|
|
453
|
+
mentionIndex = 0;
|
|
454
|
+
mentionPopup.innerHTML = '';
|
|
455
|
+
if (mentionItems.length === 0) {
|
|
456
|
+
const empty = document.createElement('div');
|
|
457
|
+
empty.className = 'mention-empty';
|
|
458
|
+
empty.textContent = i18n.noMatchFiles || 'No matching files';
|
|
459
|
+
mentionPopup.appendChild(empty);
|
|
460
|
+
} else {
|
|
461
|
+
mentionItems.forEach((it, i) => {
|
|
462
|
+
const el = document.createElement('div');
|
|
463
|
+
el.className = 'mention-item' + (i === 0 ? ' active' : '');
|
|
464
|
+
el.setAttribute('role', 'option');
|
|
465
|
+
const icon = document.createElement('span');
|
|
466
|
+
icon.className = 'mention-item__icon';
|
|
467
|
+
icon.appendChild(svg(isDirPath(it.path) ? ICONS.folder : ICONS.file));
|
|
468
|
+
const name = document.createElement('span');
|
|
469
|
+
name.className = 'mention-item__name';
|
|
470
|
+
name.textContent = it.name;
|
|
471
|
+
const rel = document.createElement('span');
|
|
472
|
+
rel.className = 'mention-item__path';
|
|
473
|
+
rel.textContent = it.rel;
|
|
474
|
+
el.appendChild(icon); el.appendChild(name); el.appendChild(rel);
|
|
475
|
+
el.addEventListener('mousedown', (ev) => { ev.preventDefault(); selectMention(i); });
|
|
476
|
+
mentionPopup.appendChild(el);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
positionMention();
|
|
480
|
+
mentionPopup.classList.remove('hidden');
|
|
481
|
+
mentionOpen = true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function positionMention() {
|
|
485
|
+
const r = feedbackInput.getBoundingClientRect();
|
|
486
|
+
const spaceAbove = r.top;
|
|
487
|
+
mentionPopup.style.left = r.left + 'px';
|
|
488
|
+
mentionPopup.style.width = r.width + 'px';
|
|
489
|
+
if (spaceAbove > 160) {
|
|
490
|
+
mentionPopup.style.bottom = (window.innerHeight - r.top + 4) + 'px';
|
|
491
|
+
mentionPopup.style.top = 'auto';
|
|
492
|
+
mentionPopup.style.maxHeight = Math.min(240, spaceAbove - 12) + 'px';
|
|
493
|
+
} else {
|
|
494
|
+
mentionPopup.style.top = (r.bottom + 4) + 'px';
|
|
495
|
+
mentionPopup.style.bottom = 'auto';
|
|
496
|
+
mentionPopup.style.maxHeight = Math.min(240, window.innerHeight - r.bottom - 12) + 'px';
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function moveMention(delta) {
|
|
501
|
+
const items = mentionPopup.querySelectorAll('.mention-item');
|
|
502
|
+
if (!items.length) return;
|
|
503
|
+
const prev = items[mentionIndex];
|
|
504
|
+
if (prev) prev.classList.remove('active');
|
|
505
|
+
mentionIndex = (mentionIndex + delta + items.length) % items.length;
|
|
506
|
+
const cur = items[mentionIndex];
|
|
507
|
+
cur.classList.add('active');
|
|
508
|
+
cur.scrollIntoView({ block: 'nearest' });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function selectMention(i) {
|
|
512
|
+
const it = mentionItems[i];
|
|
513
|
+
if (!it) { closeMention(); return; }
|
|
514
|
+
addAttachedFile(it.path);
|
|
515
|
+
const val = feedbackInput.value;
|
|
516
|
+
const pos = feedbackInput.selectionStart;
|
|
517
|
+
feedbackInput.value = val.slice(0, mentionStart) + val.slice(pos);
|
|
518
|
+
feedbackInput.selectionStart = feedbackInput.selectionEnd = mentionStart;
|
|
519
|
+
mentionDismissedAt = -1;
|
|
520
|
+
closeMention();
|
|
521
|
+
saveDraft();
|
|
522
|
+
updateCharCount();
|
|
523
|
+
feedbackInput.focus();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function closeMention() {
|
|
527
|
+
mentionOpen = false;
|
|
528
|
+
mentionPopup.classList.add('hidden');
|
|
529
|
+
pendingMentionQuery = null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
document.addEventListener('click', (e) => {
|
|
533
|
+
if (mentionOpen && !mentionPopup.contains(e.target) && e.target !== feedbackInput) {
|
|
534
|
+
closeMention();
|
|
535
|
+
}
|
|
138
536
|
});
|
|
139
537
|
|
|
140
|
-
//
|
|
538
|
+
// 压缩图片:限制最大边 + 转 JPEG,显著减小 base64 体积,节约 AI 上下文。
|
|
539
|
+
// 注意:用 JPEG 而非 WebP —— 反馈图片要经过 MCP server 处理,而其图片链路未必注册 webp codec。
|
|
540
|
+
function compressImage(dataUrl, fallbackName) {
|
|
541
|
+
return new Promise((resolve) => {
|
|
542
|
+
const img = new Image();
|
|
543
|
+
img.onload = () => {
|
|
544
|
+
try {
|
|
545
|
+
const MAX = 1568;
|
|
546
|
+
let w = img.naturalWidth || img.width;
|
|
547
|
+
let h = img.naturalHeight || img.height;
|
|
548
|
+
if (!w || !h) { resolve({ dataUrl: dataUrl, name: fallbackName }); return; }
|
|
549
|
+
if (w > MAX || h > MAX) {
|
|
550
|
+
const r = Math.min(MAX / w, MAX / h);
|
|
551
|
+
w = Math.round(w * r); h = Math.round(h * r);
|
|
552
|
+
}
|
|
553
|
+
const canvas = document.createElement('canvas');
|
|
554
|
+
canvas.width = w; canvas.height = h;
|
|
555
|
+
const ctx = canvas.getContext('2d');
|
|
556
|
+
// 白底:避免透明 PNG 转 JPEG 后透明区域变黑
|
|
557
|
+
ctx.fillStyle = '#ffffff';
|
|
558
|
+
ctx.fillRect(0, 0, w, h);
|
|
559
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
560
|
+
const out = canvas.toDataURL('image/jpeg', 0.85);
|
|
561
|
+
// 压缩反而更大时(少数小图),保留原图
|
|
562
|
+
if (out.length >= dataUrl.length) { resolve({ dataUrl: dataUrl, name: fallbackName }); return; }
|
|
563
|
+
const base = (fallbackName || ('image-' + Date.now())).replace(/\.\w+$/, '');
|
|
564
|
+
resolve({ dataUrl: out, name: base + '.jpg' });
|
|
565
|
+
} catch (e) {
|
|
566
|
+
resolve({ dataUrl: dataUrl, name: fallbackName });
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
img.onerror = () => resolve({ dataUrl: dataUrl, name: fallbackName });
|
|
570
|
+
img.src = dataUrl;
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function renderImagePreview(imgData) {
|
|
575
|
+
const item = document.createElement('div');
|
|
576
|
+
item.className = 'preview';
|
|
577
|
+
|
|
578
|
+
const img = document.createElement('img');
|
|
579
|
+
img.src = imgData.dataUrl;
|
|
580
|
+
img.alt = imgData.name;
|
|
581
|
+
img.title = i18n.clickToZoom || 'Click to zoom';
|
|
582
|
+
img.addEventListener('click', () => openLightbox(imgData.dataUrl));
|
|
583
|
+
|
|
584
|
+
item.appendChild(img);
|
|
585
|
+
item.appendChild(makeRemoveBtn(() => {
|
|
586
|
+
const index = uploadedImages.indexOf(imgData);
|
|
587
|
+
if (index > -1) uploadedImages.splice(index, 1);
|
|
588
|
+
item.remove();
|
|
589
|
+
saveDraft();
|
|
590
|
+
}));
|
|
591
|
+
imagePreview.appendChild(item);
|
|
592
|
+
}
|
|
593
|
+
|
|
141
594
|
function addImageFile(file) {
|
|
142
595
|
const reader = new FileReader();
|
|
143
|
-
reader.onload = (e) => {
|
|
144
|
-
const
|
|
596
|
+
reader.onload = async (e) => {
|
|
597
|
+
const origName = file.name || ('pasted-image-' + Date.now() + '.png');
|
|
598
|
+
const { dataUrl, name } = await compressImage(e.target.result, origName);
|
|
145
599
|
const imgData = {
|
|
146
|
-
name:
|
|
147
|
-
|
|
148
|
-
size:
|
|
600
|
+
name: name,
|
|
601
|
+
dataUrl: dataUrl,
|
|
602
|
+
size: Math.round((dataUrl.length - dataUrl.indexOf(',') - 1) * 0.75)
|
|
149
603
|
};
|
|
150
604
|
uploadedImages.push(imgData);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
container.className = 'image-preview-item';
|
|
154
|
-
|
|
155
|
-
const img = document.createElement('img');
|
|
156
|
-
img.src = base64;
|
|
157
|
-
|
|
158
|
-
const removeBtn = document.createElement('button');
|
|
159
|
-
removeBtn.className = 'image-remove';
|
|
160
|
-
removeBtn.textContent = '×';
|
|
161
|
-
removeBtn.onclick = () => {
|
|
162
|
-
const index = uploadedImages.indexOf(imgData);
|
|
163
|
-
if (index > -1) uploadedImages.splice(index, 1);
|
|
164
|
-
container.remove();
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
container.appendChild(img);
|
|
168
|
-
container.appendChild(removeBtn);
|
|
169
|
-
imagePreview.appendChild(container);
|
|
605
|
+
renderImagePreview(imgData);
|
|
606
|
+
saveDraft();
|
|
170
607
|
};
|
|
171
608
|
reader.readAsDataURL(file);
|
|
172
609
|
}
|
|
173
610
|
|
|
174
|
-
|
|
611
|
+
imageInput.addEventListener('change', (e) => {
|
|
612
|
+
for (const file of e.target.files) addImageFile(file);
|
|
613
|
+
imageInput.value = '';
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// ---------- Paste image ----------
|
|
175
617
|
document.addEventListener('paste', (e) => {
|
|
176
|
-
const items = e.clipboardData
|
|
618
|
+
const items = e.clipboardData && e.clipboardData.items;
|
|
177
619
|
if (!items) return;
|
|
178
620
|
for (const item of items) {
|
|
179
621
|
if (item.type.startsWith('image/')) {
|
|
@@ -184,103 +626,230 @@
|
|
|
184
626
|
}
|
|
185
627
|
});
|
|
186
628
|
|
|
187
|
-
//
|
|
629
|
+
// ---------- Drag & drop images ----------
|
|
630
|
+
let dragDepth = 0;
|
|
631
|
+
function hasFiles(e) {
|
|
632
|
+
return e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files');
|
|
633
|
+
}
|
|
634
|
+
window.addEventListener('dragenter', (e) => {
|
|
635
|
+
if (!hasFiles(e) || feedbackForm.classList.contains('hidden')) return;
|
|
636
|
+
e.preventDefault();
|
|
637
|
+
dragDepth++;
|
|
638
|
+
dropOverlay.classList.remove('hidden');
|
|
639
|
+
});
|
|
640
|
+
window.addEventListener('dragover', (e) => {
|
|
641
|
+
if (!hasFiles(e) || feedbackForm.classList.contains('hidden')) return;
|
|
642
|
+
e.preventDefault();
|
|
643
|
+
});
|
|
644
|
+
window.addEventListener('dragleave', (e) => {
|
|
645
|
+
if (!hasFiles(e)) return;
|
|
646
|
+
dragDepth = Math.max(0, dragDepth - 1);
|
|
647
|
+
if (dragDepth === 0) dropOverlay.classList.add('hidden');
|
|
648
|
+
});
|
|
649
|
+
window.addEventListener('drop', (e) => {
|
|
650
|
+
dragDepth = 0;
|
|
651
|
+
dropOverlay.classList.add('hidden');
|
|
652
|
+
if (!e.dataTransfer || feedbackForm.classList.contains('hidden')) return;
|
|
653
|
+
e.preventDefault();
|
|
654
|
+
for (const file of e.dataTransfer.files) {
|
|
655
|
+
if (file.type.startsWith('image/')) addImageFile(file);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// ---------- Resizable summary / input panels ----------
|
|
660
|
+
// 存「比例」而非绝对 px:webview 的 localStorage 多窗口共享,存绝对 px 会把大屏拖的高度
|
|
661
|
+
// 串到小屏。改存占窗口高度的比例后,各屏按各自高度还原,比例一致、窗口缩放也跟着走。
|
|
662
|
+
const SUMMARY_RATIO_KEY = 'cursorFeedback_summaryRatio';
|
|
663
|
+
const DEFAULT_SUMMARY_RATIO = 0.42;
|
|
664
|
+
|
|
665
|
+
// Upper bound keeps room for the input box + the pinned submit bar below.
|
|
666
|
+
function splitMax() { return Math.max(120, window.innerHeight - 230); }
|
|
667
|
+
|
|
668
|
+
function applySummaryHeight(px) {
|
|
669
|
+
const h = Math.max(48, Math.min(px, splitMax()));
|
|
670
|
+
summaryCard.style.height = h + 'px';
|
|
671
|
+
return h;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function restoreSummaryHeight() {
|
|
675
|
+
const saved = parseFloat(localStorage.getItem(SUMMARY_RATIO_KEY) || '');
|
|
676
|
+
const ratio = saved > 0 && saved < 1 ? saved : DEFAULT_SUMMARY_RATIO;
|
|
677
|
+
applySummaryHeight(Math.round(window.innerHeight * ratio));
|
|
678
|
+
}
|
|
679
|
+
restoreSummaryHeight();
|
|
680
|
+
|
|
681
|
+
let splitDragging = false;
|
|
682
|
+
let splitStartY = 0;
|
|
683
|
+
let splitStartH = 0;
|
|
684
|
+
|
|
685
|
+
splitter.addEventListener('mousedown', (e) => {
|
|
686
|
+
splitDragging = true;
|
|
687
|
+
splitStartY = e.clientY;
|
|
688
|
+
splitStartH = summaryCard.getBoundingClientRect().height;
|
|
689
|
+
splitter.classList.add('is-dragging');
|
|
690
|
+
document.body.style.userSelect = 'none';
|
|
691
|
+
document.body.style.cursor = 'row-resize';
|
|
692
|
+
e.preventDefault();
|
|
693
|
+
});
|
|
694
|
+
window.addEventListener('mousemove', (e) => {
|
|
695
|
+
if (!splitDragging) return;
|
|
696
|
+
applySummaryHeight(splitStartH + (e.clientY - splitStartY));
|
|
697
|
+
});
|
|
698
|
+
window.addEventListener('mouseup', () => {
|
|
699
|
+
if (!splitDragging) return;
|
|
700
|
+
splitDragging = false;
|
|
701
|
+
splitter.classList.remove('is-dragging');
|
|
702
|
+
document.body.style.userSelect = '';
|
|
703
|
+
document.body.style.cursor = '';
|
|
704
|
+
const ratio = summaryCard.getBoundingClientRect().height / window.innerHeight;
|
|
705
|
+
localStorage.setItem(SUMMARY_RATIO_KEY, ratio.toFixed(4));
|
|
706
|
+
});
|
|
707
|
+
// 切换侧边栏 / 窗口缩放后用保存值恢复;不能读瞬时 getBoundingClientRect(隐藏态布局会把摘要异常压扁)
|
|
708
|
+
window.addEventListener('resize', restoreSummaryHeight);
|
|
709
|
+
document.addEventListener('visibilitychange', () => {
|
|
710
|
+
if (!document.hidden) restoreSummaryHeight();
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// ---------- Countdown + progress bar ----------
|
|
188
714
|
function updateCountdown() {
|
|
189
715
|
if (!requestTimestamp || !requestTimeout) return;
|
|
190
716
|
const elapsed = Math.floor((Date.now() - requestTimestamp) / 1000);
|
|
191
717
|
const remaining = Math.max(0, requestTimeout - elapsed);
|
|
192
718
|
const minutes = Math.floor(remaining / 60);
|
|
193
719
|
const seconds = remaining % 60;
|
|
194
|
-
const
|
|
195
|
-
|
|
720
|
+
const ratio = Math.max(0, Math.min(1, remaining / requestTimeout));
|
|
721
|
+
|
|
722
|
+
timeoutBar.style.width = (ratio * 100) + '%';
|
|
723
|
+
timeoutWrap.classList.toggle('is-warning', ratio <= 0.25 && ratio > 0.1);
|
|
724
|
+
timeoutWrap.classList.toggle('is-danger', ratio <= 0.1);
|
|
725
|
+
|
|
726
|
+
const label = i18n.remainingTime || 'Remaining time';
|
|
727
|
+
timeoutInfo.textContent = label + ' ' + minutes + ':' + seconds.toString().padStart(2, '0');
|
|
728
|
+
|
|
196
729
|
if (remaining <= 0) {
|
|
197
730
|
clearInterval(countdownInterval);
|
|
731
|
+
countdownInterval = null;
|
|
198
732
|
timeoutInfo.textContent = i18n.timeout || 'Timeout';
|
|
199
733
|
}
|
|
200
734
|
}
|
|
201
735
|
|
|
202
|
-
//
|
|
736
|
+
// ---------- Submit ----------
|
|
737
|
+
function setSubmitting(on) {
|
|
738
|
+
if (on) {
|
|
739
|
+
submitBtn.classList.add('is-loading');
|
|
740
|
+
submitBtn.disabled = true;
|
|
741
|
+
submitLabel.textContent = i18n.submitting || 'Submitting…';
|
|
742
|
+
if (submitKbd) submitKbd.style.display = 'none';
|
|
743
|
+
} else {
|
|
744
|
+
submitBtn.classList.remove('is-loading');
|
|
745
|
+
submitBtn.disabled = false;
|
|
746
|
+
if (submitKbd) submitKbd.style.display = '';
|
|
747
|
+
updateKeyModeUI();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function resetForm() {
|
|
752
|
+
feedbackInput.value = '';
|
|
753
|
+
uploadedImages = [];
|
|
754
|
+
attachedFiles = [];
|
|
755
|
+
codeRefs = [];
|
|
756
|
+
imagePreview.innerHTML = '';
|
|
757
|
+
refChips.innerHTML = '';
|
|
758
|
+
currentRequestId = '';
|
|
759
|
+
updateCharCount();
|
|
760
|
+
vscode.setState({});
|
|
761
|
+
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 提交时把代码引用 chip 拼成 markdown 代码块附到反馈文本末尾
|
|
765
|
+
function buildFeedbackText() {
|
|
766
|
+
let text = feedbackInput.value.trim();
|
|
767
|
+
if (codeRefs.length) {
|
|
768
|
+
const blocks = codeRefs.map((r) => {
|
|
769
|
+
const lines = r.startLine === r.endLine ? String(r.startLine) : (r.startLine + '-' + r.endLine);
|
|
770
|
+
return '`' + r.fileName + ':' + lines + '`:\n```' + (r.lang || '') + '\n' + r.code + '\n```';
|
|
771
|
+
});
|
|
772
|
+
text += (text ? '\n\n' : '') + blocks.join('\n\n');
|
|
773
|
+
}
|
|
774
|
+
return text;
|
|
775
|
+
}
|
|
776
|
+
|
|
203
777
|
function submitFeedback() {
|
|
204
|
-
if (!currentRequestId) return;
|
|
205
|
-
|
|
778
|
+
if (!currentRequestId || submitBtn.disabled) return;
|
|
779
|
+
|
|
780
|
+
setSubmitting(true);
|
|
206
781
|
vscode.postMessage({
|
|
207
782
|
type: 'submitFeedback',
|
|
208
783
|
payload: {
|
|
209
784
|
requestId: currentRequestId,
|
|
210
|
-
interactive_feedback:
|
|
211
|
-
images: uploadedImages,
|
|
785
|
+
interactive_feedback: buildFeedbackText(),
|
|
786
|
+
images: uploadedImages.map(im => ({ name: im.name, data: im.dataUrl.split(',')[1], size: im.size })),
|
|
212
787
|
attachedFiles: attachedFiles,
|
|
213
788
|
project_directory: currentProjectDir
|
|
214
789
|
}
|
|
215
790
|
});
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
attachedFiles = [];
|
|
221
|
-
imagePreview.innerHTML = '';
|
|
222
|
-
fileList.innerHTML = '';
|
|
223
|
-
currentRequestId = '';
|
|
224
|
-
vscode.setState({}); // 清除保存的文本
|
|
225
|
-
|
|
226
|
-
if (countdownInterval) {
|
|
227
|
-
clearInterval(countdownInterval);
|
|
228
|
-
countdownInterval = null;
|
|
229
|
-
}
|
|
791
|
+
|
|
792
|
+
// 兜底:若 8s 内未收到等待态(例如提交失败),恢复按钮以便重试
|
|
793
|
+
if (submitFallbackTimer) clearTimeout(submitFallbackTimer);
|
|
794
|
+
submitFallbackTimer = setTimeout(() => setSubmitting(false), 8000);
|
|
230
795
|
}
|
|
231
796
|
|
|
232
797
|
submitBtn.addEventListener('click', submitFeedback);
|
|
233
798
|
feedbackInput.addEventListener('keydown', (e) => {
|
|
234
|
-
|
|
799
|
+
if (mentionOpen && !mentionPopup.classList.contains('hidden')) {
|
|
800
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); moveMention(1); return; }
|
|
801
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); moveMention(-1); return; }
|
|
802
|
+
if (e.key === 'Enter') { e.preventDefault(); selectMention(mentionIndex); return; }
|
|
803
|
+
if (e.key === 'Escape') { e.preventDefault(); mentionDismissedAt = mentionStart; closeMention(); return; }
|
|
804
|
+
}
|
|
235
805
|
if (isComposing || e.isComposing) return;
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
e.preventDefault();
|
|
242
|
-
submitFeedback();
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
// Ctrl+Enter 提交模式:Ctrl+Enter 提交,Enter 换行
|
|
246
|
-
if (e.ctrlKey || e.metaKey) {
|
|
247
|
-
e.preventDefault();
|
|
248
|
-
submitFeedback();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
806
|
+
if (e.key !== 'Enter') return;
|
|
807
|
+
if (enterToSubmit) {
|
|
808
|
+
if (!e.shiftKey) { e.preventDefault(); submitFeedback(); }
|
|
809
|
+
} else if (e.ctrlKey || e.metaKey) {
|
|
810
|
+
e.preventDefault(); submitFeedback();
|
|
251
811
|
}
|
|
252
812
|
});
|
|
253
813
|
|
|
254
|
-
//
|
|
814
|
+
// ---------- Messages from extension ----------
|
|
255
815
|
window.addEventListener('message', event => {
|
|
256
816
|
const message = event.data;
|
|
257
|
-
|
|
817
|
+
|
|
258
818
|
switch (message.type) {
|
|
259
819
|
case 'showFeedbackRequest':
|
|
260
820
|
waitingStatus.classList.add('hidden');
|
|
261
821
|
feedbackForm.classList.remove('hidden');
|
|
822
|
+
submitBar.classList.remove('hidden');
|
|
823
|
+
if (submitFallbackTimer) { clearTimeout(submitFallbackTimer); submitFallbackTimer = null; }
|
|
824
|
+
setSubmitting(false);
|
|
262
825
|
currentRequestId = message.payload.requestId;
|
|
263
826
|
currentProjectDir = message.payload.projectDir;
|
|
264
827
|
requestTimestamp = message.payload.timestamp;
|
|
265
828
|
requestTimeout = message.payload.timeout;
|
|
266
829
|
summaryContent.innerHTML = renderMarkdown(message.payload.summary);
|
|
830
|
+
highlightCodeBlocks(summaryContent);
|
|
267
831
|
summaryContent.scrollTop = 0;
|
|
268
|
-
projectInfo.textContent =
|
|
832
|
+
projectInfo.textContent = message.payload.projectDir;
|
|
833
|
+
projectInfo.title = message.payload.projectDir;
|
|
834
|
+
updateCharCount();
|
|
269
835
|
feedbackInput.focus();
|
|
836
|
+
timeoutWrap.hidden = false;
|
|
270
837
|
if (countdownInterval) clearInterval(countdownInterval);
|
|
271
838
|
updateCountdown();
|
|
272
839
|
countdownInterval = setInterval(updateCountdown, 1000);
|
|
840
|
+
requestAnimationFrame(restoreSummaryHeight);
|
|
273
841
|
break;
|
|
274
|
-
|
|
842
|
+
|
|
275
843
|
case 'showWaiting':
|
|
844
|
+
if (submitFallbackTimer) { clearTimeout(submitFallbackTimer); submitFallbackTimer = null; }
|
|
845
|
+
resetForm();
|
|
846
|
+
setSubmitting(false);
|
|
276
847
|
feedbackForm.classList.add('hidden');
|
|
848
|
+
submitBar.classList.add('hidden');
|
|
277
849
|
waitingStatus.classList.remove('hidden');
|
|
278
|
-
|
|
279
|
-
clearInterval(countdownInterval);
|
|
280
|
-
countdownInterval = null;
|
|
281
|
-
}
|
|
850
|
+
timeoutWrap.hidden = true;
|
|
282
851
|
break;
|
|
283
|
-
|
|
852
|
+
|
|
284
853
|
case 'serverStatus':
|
|
285
854
|
if (message.payload.connected) {
|
|
286
855
|
serverStatus.classList.add('connected');
|
|
@@ -290,28 +859,109 @@
|
|
|
290
859
|
serverStatusText.textContent = i18n.mcpServerDisconnected || 'MCP Server disconnected';
|
|
291
860
|
}
|
|
292
861
|
break;
|
|
293
|
-
|
|
294
|
-
case 'updateDebugInfo':
|
|
862
|
+
|
|
863
|
+
case 'updateDebugInfo': {
|
|
295
864
|
const d = message.payload;
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
865
|
+
const L = {
|
|
866
|
+
debug: i18n.debugInfo || 'Debug Info',
|
|
867
|
+
port: i18n.scanPort || 'Scan port',
|
|
868
|
+
ws: i18n.workspace || 'Workspace',
|
|
869
|
+
cur: i18n.currentPort || 'Current port',
|
|
870
|
+
conn: i18n.connected || 'Connected',
|
|
871
|
+
none: i18n.none || 'None',
|
|
872
|
+
status: i18n.status || 'Status'
|
|
873
|
+
};
|
|
874
|
+
debugTooltip.textContent =
|
|
875
|
+
`${L.debug}\n${L.port}: ${d.portRange}\n${L.ws}: ${d.workspacePath}\n` +
|
|
876
|
+
`${L.cur}: ${d.activePort || '-'}\n` +
|
|
877
|
+
`${L.conn}: ${d.connectedPorts.length > 0 ? d.connectedPorts.join(', ') : L.none}\n` +
|
|
878
|
+
`${L.status}: ${d.lastStatus}`;
|
|
304
879
|
break;
|
|
305
|
-
|
|
880
|
+
}
|
|
881
|
+
|
|
306
882
|
case 'filesSelected':
|
|
307
883
|
if (message.payload.paths) {
|
|
308
884
|
for (const path of message.payload.paths) addAttachedFile(path);
|
|
309
885
|
}
|
|
310
886
|
break;
|
|
887
|
+
|
|
888
|
+
case 'fileSearchResults':
|
|
889
|
+
allFilesCache = message.payload.files || [];
|
|
890
|
+
allFilesCacheTime = Date.now();
|
|
891
|
+
if (mentionOpen || pendingMentionQuery != null) {
|
|
892
|
+
const q = pendingMentionQuery != null ? pendingMentionQuery : '';
|
|
893
|
+
pendingMentionQuery = null;
|
|
894
|
+
renderMention(q);
|
|
895
|
+
}
|
|
896
|
+
break;
|
|
897
|
+
|
|
898
|
+
case 'insertContext': {
|
|
899
|
+
const p = message.payload;
|
|
900
|
+
const fileName = (p.filePath || '').split(/[\\/]/).pop();
|
|
901
|
+
codeRefs.push({
|
|
902
|
+
fileName: fileName,
|
|
903
|
+
filePath: p.filePath,
|
|
904
|
+
lang: p.lang || '',
|
|
905
|
+
code: p.code,
|
|
906
|
+
startLine: p.startLine,
|
|
907
|
+
endLine: p.endLine
|
|
908
|
+
});
|
|
909
|
+
renderRefChips();
|
|
910
|
+
saveDraft();
|
|
911
|
+
feedbackInput.focus();
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
case 'insertContextEmpty':
|
|
916
|
+
break;
|
|
917
|
+
|
|
918
|
+
case 'autoRetryState':
|
|
919
|
+
updateAutoRetryUI(!!message.payload.enabled);
|
|
920
|
+
break;
|
|
921
|
+
|
|
922
|
+
case 'feishuState':
|
|
923
|
+
updateFeishuUI(message.payload);
|
|
924
|
+
break;
|
|
311
925
|
}
|
|
312
926
|
});
|
|
313
927
|
|
|
314
|
-
//
|
|
928
|
+
// ---------- Init ----------
|
|
929
|
+
(function restoreDraft() {
|
|
930
|
+
const st = previousState;
|
|
931
|
+
if (!st) return;
|
|
932
|
+
if (st.text) feedbackInput.value = st.text;
|
|
933
|
+
if (Array.isArray(st.images)) {
|
|
934
|
+
for (const im of st.images) {
|
|
935
|
+
if (im && im.dataUrl) { uploadedImages.push(im); renderImagePreview(im); }
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (Array.isArray(st.files)) {
|
|
939
|
+
for (const p of st.files) {
|
|
940
|
+
if (p && !attachedFiles.includes(p)) attachedFiles.push(p);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (Array.isArray(st.codeRefs)) {
|
|
944
|
+
for (const r of st.codeRefs) { if (r) codeRefs.push(r); }
|
|
945
|
+
}
|
|
946
|
+
renderRefChips();
|
|
947
|
+
})();
|
|
948
|
+
|
|
949
|
+
// 把快捷键用 kbd 框起来,让 ⇧⌘' 里那个 ' 更醒目
|
|
950
|
+
(function renderContextHint() {
|
|
951
|
+
const hint = document.querySelector('.composer__hint');
|
|
952
|
+
if (!hint) return;
|
|
953
|
+
const mention = i18n.contextHintMention || '@ files';
|
|
954
|
+
const insert = i18n.contextHintInsert || '';
|
|
955
|
+
hint.textContent = '';
|
|
956
|
+
hint.appendChild(document.createTextNode(mention + ' · '));
|
|
957
|
+
const kbd = document.createElement('kbd');
|
|
958
|
+
kbd.className = 'hint-kbd';
|
|
959
|
+
kbd.textContent = i18n.insertShortcut || '';
|
|
960
|
+
hint.appendChild(kbd);
|
|
961
|
+
hint.appendChild(document.createTextNode(' ' + insert));
|
|
962
|
+
})();
|
|
963
|
+
|
|
964
|
+
updateCharCount();
|
|
315
965
|
setInterval(() => vscode.postMessage({ type: 'checkServer' }), 5000);
|
|
316
966
|
vscode.postMessage({ type: 'ready' });
|
|
317
967
|
vscode.postMessage({ type: 'checkServer' });
|