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.
@@ -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
- // Markdown 渲染
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
21
41
  }
22
42
 
23
- // DOM 元素
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 fileList = document.getElementById('fileList');
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
- langSwitchBtn.addEventListener('click', () => {
44
- vscode.postMessage({ type: 'switchLanguage' });
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
- // 快捷键模式:false = Ctrl+Enter 提交(默认),true = Enter 提交
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
- submitBtn.textContent = i18n.enterSubmitMode || 'Enter to submit · Shift+Enter for newline';
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
- submitBtn.textContent = i18n.ctrlEnterSubmitMode || 'Ctrl+Enter to submit · Enter for newline';
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
- isComposing = true;
85
- });
86
- feedbackInput.addEventListener('compositionend', () => {
87
- isComposing = false;
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
- if (previousState?.text) {
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
- vscode.setState({ text: feedbackInput.value });
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
- const item = document.createElement('div');
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
- imageInput.addEventListener('change', (e) => {
137
- for (const file of e.target.files) addImageFile(file);
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 base64 = e.target.result;
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: file.name || ('pasted-image-' + Date.now() + '.png'),
147
- data: base64.split(',')[1],
148
- size: file.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
- const container = document.createElement('div');
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?.items;
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 remainingLabel = i18n.remainingTime || 'Remaining time';
195
- timeoutInfo.textContent = remainingLabel + ': ' + minutes + ':' + seconds.toString().padStart(2, '0');
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: feedbackInput.value.trim(),
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
- feedbackInput.value = '';
219
- uploadedImages = [];
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 (e.key === 'Enter') {
238
- if (enterToSubmit) {
239
- // Enter 提交模式:Enter 提交,Shift+Enter 换行
240
- if (!e.shiftKey) {
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 = '📁 ' + message.payload.projectDir;
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
- if (countdownInterval) {
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 debugLabel = i18n.debugInfo || 'Debug Info';
297
- const scanPortLabel = i18n.scanPort || 'Scan port';
298
- const workspaceLabel = i18n.workspace || 'Workspace';
299
- const currentPortLabel = i18n.currentPort || 'Current port';
300
- const connectedLabel = i18n.connected || 'Connected';
301
- const noneLabel = i18n.none || 'None';
302
- const statusLabel = i18n.status || 'Status';
303
- debugTooltip.textContent = `🔍 ${debugLabel}\n━━━━━━━━━━━━\n${scanPortLabel}: ${d.portRange}\n${workspaceLabel}: ${d.workspacePath}\n${currentPortLabel}: ${d.activePort || '-'}\n${connectedLabel}: ${d.connectedPorts.length > 0 ? d.connectedPorts.join(', ') : noneLabel}\n${statusLabel}: ${d.lastStatus}`;
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' });