cursor-feedback 2.1.2 → 2.3.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.
@@ -92,12 +92,12 @@
92
92
  const feishuAppSecretInput = document.getElementById('feishuAppSecret');
93
93
  const feishuStatusEl = document.querySelector('.feishu-status');
94
94
  const feishuStatusText = document.getElementById('feishuStatusText');
95
- const feishuGuideBtn = document.getElementById('feishuGuideBtn');
96
95
  const feishuSecretToggle = document.getElementById('feishuSecretToggle');
97
96
  const feishuEnabledToggle = document.getElementById('feishuEnabledToggle');
98
97
  const systemNotifyToggle = document.getElementById('systemNotifyToggle');
99
98
  const osNotifyToggle = document.getElementById('osNotifyToggle');
100
99
  const feishuAckToggle = document.getElementById('feishuAckToggle');
100
+ const queueWhenBusyToggle = document.getElementById('queueWhenBusyToggle');
101
101
  const osNotifySub = document.getElementById('osNotifySub');
102
102
  const feishuAckSub = document.getElementById('feishuAckSub');
103
103
  const notifyTestBtn = document.getElementById('notifyTestBtn');
@@ -105,12 +105,27 @@
105
105
  const historyBtn = document.getElementById('historyBtn');
106
106
  const historyPopup = document.getElementById('historyPopup');
107
107
  const toastEl = document.getElementById('toast');
108
+ const queueCard = document.getElementById('queueCard');
109
+ const queueList = document.getElementById('queueList');
110
+ const queueCount = document.getElementById('queueCount');
111
+ const waitingHintEl = waitingStatus.querySelector('.empty__hint');
112
+ const defaultWaitingHint = waitingHintEl ? waitingHintEl.textContent : '';
113
+ const summaryNav = document.getElementById('summaryNav');
114
+ const summaryPrevBtn = document.getElementById('summaryPrevBtn');
115
+ const summaryNextBtn = document.getElementById('summaryNextBtn');
116
+ const summaryNavPos = document.getElementById('summaryNavPos');
108
117
 
109
118
  let uploadedImages = [];
110
119
  let attachedFiles = [];
111
120
  let codeRefs = [];
112
121
  let currentRequestId = '';
113
122
  let currentProjectDir = '';
123
+ // 排队模式:AI 正忙(无等待中的反馈请求)时,同一个输入框用于「排队发送」,
124
+ // 消息进服务端忙时队列,与飞书消息同队列按序送达
125
+ let queueMode = false;
126
+ let queueItems = [];
127
+ // 忙时排队全局开关(真相在 server,随 feishuState 同步):关了则等待态回到纯等待、不提供排队输入
128
+ let queueEnabled = true;
114
129
  let requestTimestamp = 0;
115
130
  let requestTimeout = 300;
116
131
  let countdownInterval = null;
@@ -196,6 +211,8 @@
196
211
  function closeFeishuModal() {
197
212
  // 兜底:Esc 关闭等场景输入框可能不触发 blur,关闭前再存一次(已去重,不会重复提交)
198
213
  saveFeishuConfigFromInputs();
214
+ // 扫码创建流程进行中:关闭弹窗即取消(server 侧 abort,二维码作废)
215
+ resetRegisterPanel(true);
199
216
  feishuModal.classList.add('hidden');
200
217
  feishuModal.setAttribute('aria-hidden', 'true');
201
218
  }
@@ -210,10 +227,82 @@
210
227
  closeFeishuModal();
211
228
  }
212
229
  });
213
- feishuGuideBtn.addEventListener('click', () => {
214
- vscode.postMessage({ type: 'openFeishuGuide' });
230
+
231
+ // ---------- 扫码一键创建飞书应用 ----------
232
+ // 点「扫码快速创建」→ extension 向 server 发起 Device Grant 流程 → 面板展示二维码 →
233
+ // 用户在飞书确认 → 凭证自动写入 server 磁盘并生效,这里刷新输入框回显
234
+ const feishuRegisterBtn = document.getElementById('feishuRegisterBtn');
235
+ const feishuRegisterPanel = document.getElementById('feishuRegisterPanel');
236
+ const feishuRegisterQr = document.getElementById('feishuRegisterQr');
237
+ const feishuRegisterHint = document.getElementById('feishuRegisterHint');
238
+ const feishuRegisterOpenBtn = document.getElementById('feishuRegisterOpenBtn');
239
+ const feishuRegisterRetryBtn = document.getElementById('feishuRegisterRetryBtn');
240
+ let registerActive = false;
241
+ let registerUrl = '';
242
+
243
+ function resetRegisterPanel(cancelServer) {
244
+ if (cancelServer && registerActive) {
245
+ vscode.postMessage({ type: 'feishuRegisterCancel' });
246
+ }
247
+ registerActive = false;
248
+ registerUrl = '';
249
+ feishuRegisterPanel.classList.add('hidden');
250
+ feishuRegisterQr.classList.add('hidden');
251
+ feishuRegisterOpenBtn.classList.add('hidden');
252
+ feishuRegisterRetryBtn.classList.add('hidden');
253
+ feishuRegisterHint.textContent = '';
254
+ }
255
+
256
+ function startRegister() {
257
+ registerActive = true;
258
+ feishuRegisterPanel.classList.remove('hidden');
259
+ feishuRegisterQr.classList.add('hidden');
260
+ feishuRegisterOpenBtn.classList.add('hidden');
261
+ feishuRegisterRetryBtn.classList.add('hidden');
262
+ feishuRegisterHint.textContent = i18n.registerLoading || 'Getting QR code…';
263
+ vscode.postMessage({ type: 'feishuRegisterStart' });
264
+ }
265
+
266
+ feishuRegisterBtn.addEventListener('click', () => {
267
+ if (registerActive) return;
268
+ startRegister();
269
+ });
270
+ feishuRegisterRetryBtn.addEventListener('click', startRegister);
271
+ feishuRegisterOpenBtn.addEventListener('click', () => {
272
+ if (registerUrl) vscode.postMessage({ type: 'openLink', payload: { url: registerUrl } });
215
273
  });
216
274
 
275
+ function handleRegisterState(p) {
276
+ if (!registerActive || !p) return;
277
+ if (p.status === 'waiting') {
278
+ registerUrl = p.url || '';
279
+ if (p.qr) {
280
+ feishuRegisterQr.src = p.qr;
281
+ feishuRegisterQr.classList.remove('hidden');
282
+ }
283
+ feishuRegisterOpenBtn.classList.toggle('hidden', !registerUrl);
284
+ feishuRegisterHint.textContent = i18n.registerScanHint || 'Scan with Feishu to create the app';
285
+ } else if (p.status === 'success') {
286
+ registerActive = false;
287
+ registerUrl = '';
288
+ feishuRegisterQr.classList.add('hidden');
289
+ feishuRegisterOpenBtn.classList.add('hidden');
290
+ feishuRegisterHint.textContent = (i18n.registerSuccess || 'Created! Credentials configured') +
291
+ (p.appId ? ' (' + p.appId + ')' : '');
292
+ // 凭证已写入 server 磁盘,等常规轮询把新凭证同步到 feishuLastState 后刷新输入框回显
293
+ setTimeout(() => { fillFeishuInputs(); }, 1600);
294
+ setTimeout(() => { fillFeishuInputs(); }, 4000); // 兜底再刷一次(轮询同步可能略慢)
295
+ } else {
296
+ // error
297
+ feishuRegisterQr.classList.add('hidden');
298
+ feishuRegisterOpenBtn.classList.add('hidden');
299
+ feishuRegisterRetryBtn.classList.remove('hidden');
300
+ feishuRegisterHint.textContent = (i18n.registerFailed || 'Failed') + (p.error ? ': ' + p.error : '');
301
+ registerActive = false;
302
+ registerUrl = '';
303
+ }
304
+ }
305
+
217
306
  // 密钥显示/隐藏切换(小眼睛)
218
307
  feishuSecretToggle.addEventListener('click', () => {
219
308
  const show = feishuAppSecretInput.type === 'password';
@@ -239,6 +328,12 @@
239
328
  feishuAckToggle.addEventListener('change', () => {
240
329
  vscode.postMessage({ type: 'toggleFeishuAck', payload: { enabled: feishuAckToggle.checked } });
241
330
  });
331
+ // 忙时排队是全局开关(飞书 + 面板共用队列),不受飞书主开关约束
332
+ queueWhenBusyToggle.addEventListener('change', () => {
333
+ vscode.postMessage({ type: 'toggleFeishuQueue', payload: { enabled: queueWhenBusyToggle.checked } });
334
+ queueEnabled = queueWhenBusyToggle.checked;
335
+ if (queueMode) enterQueueMode(); // 立即按新开关刷新等待态形态
336
+ });
242
337
 
243
338
  // 发送测试通知:一键验证系统通知链路通不通(权限被拒时收不到,配合上方排查提示定位)
244
339
  let notifyTestHintTimer = null;
@@ -276,6 +371,13 @@
276
371
  if (typeof state.feishuAck === 'boolean') {
277
372
  feishuAckToggle.checked = state.feishuAck;
278
373
  }
374
+ if (typeof state.feishuQueue === 'boolean') {
375
+ queueWhenBusyToggle.checked = state.feishuQueue;
376
+ if (state.feishuQueue !== queueEnabled) {
377
+ queueEnabled = state.feishuQueue;
378
+ if (queueMode) enterQueueMode(); // 开关在别的窗口/server 侧变化:同步刷新等待态形态
379
+ }
380
+ }
279
381
  syncSubSwitchDisabled();
280
382
  feishuStatusEl.classList.toggle('is-configured', !!state.configured && !state.bound);
281
383
  feishuStatusEl.classList.toggle('is-bound', !!state.bound);
@@ -303,7 +405,9 @@
303
405
  let enterToSubmit = localStorage.getItem('cursorFeedback_enterToSubmit') === 'true';
304
406
 
305
407
  function updateKeyModeUI() {
306
- submitLabel.textContent = i18n.submitFeedback || 'Submit';
408
+ submitLabel.textContent = queueMode
409
+ ? (i18n.queueSend || 'Queue Message')
410
+ : (i18n.submitFeedback || 'Submit');
307
411
  if (enterToSubmit) {
308
412
  if (submitKbd) submitKbd.textContent = i18n.submitHintEnter || 'Enter';
309
413
  toggleKeyModeBtn.classList.add('enter-mode');
@@ -701,6 +805,24 @@
701
805
  }
702
806
  restoreSummaryHeight();
703
807
 
808
+ // 排队模式下分隔条调的是「顶部等待卡片」高度。与摘要卡共用同一比例(同一存储键、同一钳制),
809
+ // 这样反馈态 ↔ 排队态切换时顶部区域高度完全一致,输入框不会上下跳动。
810
+ function applyQueueTopHeight(px) {
811
+ const h = Math.max(48, Math.min(px, splitMax()));
812
+ waitingStatus.style.height = h + 'px';
813
+ return h;
814
+ }
815
+
816
+ function clearQueueTopHeight() {
817
+ waitingStatus.style.height = '';
818
+ }
819
+
820
+ function restoreQueueTopHeight() {
821
+ const saved = parseFloat(localStorage.getItem(SUMMARY_RATIO_KEY) || '');
822
+ const ratio = saved > 0 && saved < 1 ? saved : DEFAULT_SUMMARY_RATIO;
823
+ applyQueueTopHeight(Math.round(window.innerHeight * ratio));
824
+ }
825
+
704
826
  let splitDragging = false;
705
827
  let splitStartY = 0;
706
828
  let splitStartH = 0;
@@ -708,7 +830,7 @@
708
830
  splitter.addEventListener('mousedown', (e) => {
709
831
  splitDragging = true;
710
832
  splitStartY = e.clientY;
711
- splitStartH = summaryCard.getBoundingClientRect().height;
833
+ splitStartH = (queueMode ? waitingStatus : summaryCard).getBoundingClientRect().height;
712
834
  splitter.classList.add('is-dragging');
713
835
  document.body.style.userSelect = 'none';
714
836
  document.body.style.cursor = 'row-resize';
@@ -716,7 +838,9 @@
716
838
  });
717
839
  window.addEventListener('mousemove', (e) => {
718
840
  if (!splitDragging) return;
719
- applySummaryHeight(splitStartH + (e.clientY - splitStartY));
841
+ const next = splitStartH + (e.clientY - splitStartY);
842
+ if (queueMode) applyQueueTopHeight(next);
843
+ else applySummaryHeight(next);
720
844
  });
721
845
  window.addEventListener('mouseup', () => {
722
846
  if (!splitDragging) return;
@@ -724,13 +848,20 @@
724
848
  splitter.classList.remove('is-dragging');
725
849
  document.body.style.userSelect = '';
726
850
  document.body.style.cursor = '';
727
- const ratio = summaryCard.getBoundingClientRect().height / window.innerHeight;
851
+ // 两种形态共用同一比例:任一形态下拖动都会同步到另一形态
852
+ const el = queueMode ? waitingStatus : summaryCard;
853
+ const ratio = el.getBoundingClientRect().height / window.innerHeight;
728
854
  localStorage.setItem(SUMMARY_RATIO_KEY, ratio.toFixed(4));
729
855
  });
730
856
  // 切换侧边栏 / 窗口缩放后用保存值恢复;不能读瞬时 getBoundingClientRect(隐藏态布局会把摘要异常压扁)
731
- window.addEventListener('resize', restoreSummaryHeight);
857
+ window.addEventListener('resize', () => {
858
+ restoreSummaryHeight();
859
+ if (queueMode) restoreQueueTopHeight();
860
+ });
732
861
  document.addEventListener('visibilitychange', () => {
733
- if (!document.hidden) restoreSummaryHeight();
862
+ if (document.hidden) return;
863
+ restoreSummaryHeight();
864
+ if (queueMode) restoreQueueTopHeight();
734
865
  });
735
866
 
736
867
  // ---------- Quick replies (一键发送的快捷短语,可编辑,存 localStorage) ----------
@@ -762,8 +893,10 @@
762
893
  }
763
894
 
764
895
  // 点击快捷短语=立即提交。已有草稿时把短语追加到末尾一起发,不悄悄丢弃用户已输入的内容
896
+ // (排队模式下同样可用:短语走排队发送)
765
897
  function sendQuickReply(phrase) {
766
- if (!currentRequestId || submitBtn.disabled) return;
898
+ if (submitBtn.disabled) return;
899
+ if (!currentRequestId && !queueMode) return;
767
900
  const cur = feedbackInput.value.trim();
768
901
  feedbackInput.value = cur ? cur + '\n' + phrase : phrase;
769
902
  saveDraft();
@@ -862,6 +995,52 @@
862
995
  }
863
996
  renderQuickReplies();
864
997
 
998
+ // ---------- Tooltips(data-tip 全局浮层) ----------
999
+ // 用单例 fixed 浮层代替 CSS ::after:绝对定位伪元素会被任何 overflow 祖先裁切
1000
+ //(工具条/卡片边缘的按钮 tooltip 曾被切掉半截)。fixed + 视口钳制彻底规避。
1001
+ const tipFloat = document.createElement('div');
1002
+ tipFloat.className = 'tip-float hidden';
1003
+ document.body.appendChild(tipFloat);
1004
+
1005
+ function showTipFor(el) {
1006
+ const text = el.getAttribute('data-tip');
1007
+ if (!text) { hideTip(); return; }
1008
+ tipFloat.textContent = text;
1009
+ tipFloat.classList.remove('hidden');
1010
+ const r = el.getBoundingClientRect();
1011
+ const tw = tipFloat.offsetWidth;
1012
+ const th = tipFloat.offsetHeight;
1013
+ // 默认在元素上方居中;顶部放不下则翻到下方;水平钳制在视口内
1014
+ let top = r.top - th - 6;
1015
+ if (top < 4) top = r.bottom + 6;
1016
+ let left = r.left + r.width / 2 - tw / 2;
1017
+ left = Math.max(4, Math.min(left, window.innerWidth - tw - 4));
1018
+ tipFloat.style.top = top + 'px';
1019
+ tipFloat.style.left = left + 'px';
1020
+ }
1021
+ function hideTip() {
1022
+ tipFloat.classList.add('hidden');
1023
+ }
1024
+ document.addEventListener('mouseover', (e) => {
1025
+ const t = e.target;
1026
+ const el = t && t.closest ? t.closest('[data-tip]') : null;
1027
+ if (el) showTipFor(el);
1028
+ });
1029
+ document.addEventListener('mouseout', (e) => {
1030
+ const t = e.target;
1031
+ const el = t && t.closest ? t.closest('[data-tip]') : null;
1032
+ if (!el) return;
1033
+ if (e.relatedTarget && el.contains(e.relatedTarget)) return; // 仍在元素内部移动
1034
+ hideTip();
1035
+ });
1036
+ // 点击可能切换按钮语义(如超时续期开/关会改 data-tip),立即刷新提示文本
1037
+ document.addEventListener('click', (e) => {
1038
+ const t = e.target;
1039
+ const el = t && t.closest ? t.closest('[data-tip]') : null;
1040
+ if (el) showTipFor(el);
1041
+ });
1042
+ window.addEventListener('scroll', hideTip, true);
1043
+
865
1044
  // ---------- Toast (提交结果轻提示) ----------
866
1045
  let toastTimer = null;
867
1046
  function showToast(text) {
@@ -1081,7 +1260,12 @@
1081
1260
  }
1082
1261
 
1083
1262
  function submitFeedback() {
1084
- if (!currentRequestId || submitBtn.disabled) return;
1263
+ if (submitBtn.disabled) return;
1264
+ // 无等待中的请求:排队模式下改走「排队发送」,消息进服务端忙时队列
1265
+ if (!currentRequestId) {
1266
+ if (queueMode) queueSend();
1267
+ return;
1268
+ }
1085
1269
 
1086
1270
  setSubmitting(true);
1087
1271
  // 先暂存本次文本,等 extension 确认成功后写入历史(提交失败不入历史)
@@ -1102,6 +1286,38 @@
1102
1286
  submitFallbackTimer = setTimeout(() => setSubmitting(false), 8000);
1103
1287
  }
1104
1288
 
1289
+ // 排队发送:AI 忙时把消息交给 extension 转发到归属本工作区的 server 入队
1290
+ function queueSend() {
1291
+ const text = buildFeedbackText();
1292
+ if (!text && uploadedImages.length === 0 && attachedFiles.length === 0) return;
1293
+
1294
+ setSubmitting(true);
1295
+ pendingHistoryText = text;
1296
+ vscode.postMessage({
1297
+ type: 'queueMessage',
1298
+ payload: {
1299
+ interactive_feedback: text,
1300
+ images: uploadedImages.map(im => ({ name: im.name, data: im.dataUrl.split(',')[1], size: im.size })),
1301
+ attachedFiles: attachedFiles
1302
+ }
1303
+ });
1304
+
1305
+ if (submitFallbackTimer) clearTimeout(submitFallbackTimer);
1306
+ submitFallbackTimer = setTimeout(() => setSubmitting(false), 8000);
1307
+ }
1308
+
1309
+ // 排队成功后只清空输入内容,保持排队模式(用户可能还要继续追加)
1310
+ function clearComposer() {
1311
+ feedbackInput.value = '';
1312
+ uploadedImages = [];
1313
+ attachedFiles = [];
1314
+ codeRefs = [];
1315
+ imagePreview.innerHTML = '';
1316
+ refChips.innerHTML = '';
1317
+ saveDraft();
1318
+ updateCharCount();
1319
+ }
1320
+
1105
1321
  submitBtn.addEventListener('click', submitFeedback);
1106
1322
  feedbackInput.addEventListener('keydown', (e) => {
1107
1323
  if (mentionOpen && !mentionPopup.classList.contains('hidden')) {
@@ -1119,15 +1335,158 @@
1119
1335
  }
1120
1336
  });
1121
1337
 
1338
+ // ---------- Summary history(历史摘要回看) ----------
1339
+ // 每轮 showFeedbackRequest 存一条(同文本去重:超时续期会带相同 summary 重复到来),
1340
+ // 上限 20 条;摘要卡头部的 ‹ › 在轮次间翻看,新请求到来自动跳回最新
1341
+ const SUMMARY_HISTORY_KEY = 'cursorFeedback_summaryHistory';
1342
+ const SUMMARY_HISTORY_MAX = 20;
1343
+ let summaryHistIdx = 0; // 0 = 最新(当前轮)
1344
+
1345
+ function loadSummaryHistory() {
1346
+ try {
1347
+ const arr = JSON.parse(localStorage.getItem(SUMMARY_HISTORY_KEY) || '[]');
1348
+ if (Array.isArray(arr)) {
1349
+ return arr.filter((e) => e && typeof e.text === 'string' && e.text.trim());
1350
+ }
1351
+ } catch (e) { /* ignore */ }
1352
+ return [];
1353
+ }
1354
+
1355
+ function pushSummaryHistory(text) {
1356
+ const t = (text || '').trim();
1357
+ if (!t) return;
1358
+ let list = loadSummaryHistory();
1359
+ if (list.length && list[0].text === t) {
1360
+ list[0].at = Date.now(); // 超时续期重发同一摘要:只刷新时间
1361
+ } else {
1362
+ list.unshift({ text: t, at: Date.now() });
1363
+ if (list.length > SUMMARY_HISTORY_MAX) list = list.slice(0, SUMMARY_HISTORY_MAX);
1364
+ }
1365
+ try { localStorage.setItem(SUMMARY_HISTORY_KEY, JSON.stringify(list)); } catch (e) { /* ignore */ }
1366
+ }
1367
+
1368
+ function renderSummaryAt(idx) {
1369
+ const list = loadSummaryHistory();
1370
+ if (list.length === 0) return;
1371
+ summaryHistIdx = Math.max(0, Math.min(idx, list.length - 1));
1372
+ const entry = list[summaryHistIdx];
1373
+ summaryContent.innerHTML = renderMarkdown(entry.text);
1374
+ highlightCodeBlocks(summaryContent);
1375
+ summaryContent.scrollTop = 0;
1376
+ // 只有 1 条时隐藏导航;查看历史轮次时给内容区打标(样式淡化以示区分)
1377
+ if (summaryNav) {
1378
+ summaryNav.hidden = list.length <= 1;
1379
+ if (summaryNavPos) {
1380
+ summaryNavPos.textContent = (summaryHistIdx + 1) + '/' + list.length;
1381
+ summaryNavPos.title = formatHistoryTime(entry.at);
1382
+ }
1383
+ if (summaryNextBtn) summaryNextBtn.disabled = summaryHistIdx === 0;
1384
+ if (summaryPrevBtn) summaryPrevBtn.disabled = summaryHistIdx >= list.length - 1;
1385
+ }
1386
+ summaryContent.classList.toggle('summary--past', summaryHistIdx > 0);
1387
+ }
1388
+
1389
+ if (summaryPrevBtn) summaryPrevBtn.addEventListener('click', () => renderSummaryAt(summaryHistIdx + 1));
1390
+ if (summaryNextBtn) summaryNextBtn.addEventListener('click', () => renderSummaryAt(summaryHistIdx - 1));
1391
+
1392
+ // ---------- Queue mode(AI 忙时排队) ----------
1393
+ // 排队模式 = 等待态 + 可用的输入框:摘要卡隐藏,顶部保留紧凑的等待提示与队列列表,
1394
+ // 提交按钮变「排队发送」。反馈模式 = 原有的摘要 + 提交形态。
1395
+ function enterQueueMode() {
1396
+ queueMode = true;
1397
+ waitingStatus.classList.remove('hidden');
1398
+ summaryCard.classList.add('hidden');
1399
+ // 有输入框时保留分隔条,可拖动调整「等待区 : 输入区」比例;纯等待态无可调对象
1400
+ splitter.classList.toggle('hidden', !queueEnabled);
1401
+ // 占位隐藏(保持行高):倒计时行若整行消失,提交栏高度变化会让输入框上下跳动
1402
+ timeoutWrap.hidden = false;
1403
+ timeoutWrap.classList.add('countdown--idle');
1404
+ // 忙时排队开关(全局)关闭:纯等待态,不提供排队输入
1405
+ document.body.classList.toggle('queue-mode', queueEnabled);
1406
+ if (queueEnabled) restoreQueueTopHeight();
1407
+ else clearQueueTopHeight();
1408
+ if (waitingHintEl) {
1409
+ waitingHintEl.textContent = queueEnabled ? (i18n.queueModeHint || defaultWaitingHint) : defaultWaitingHint;
1410
+ }
1411
+ feedbackForm.classList.toggle('hidden', !queueEnabled);
1412
+ submitBar.classList.toggle('hidden', !queueEnabled);
1413
+ renderQueueList();
1414
+ updateKeyModeUI();
1415
+ }
1416
+
1417
+ function enterFeedbackMode() {
1418
+ queueMode = false;
1419
+ document.body.classList.remove('queue-mode');
1420
+ waitingStatus.classList.add('hidden');
1421
+ clearQueueTopHeight();
1422
+ if (waitingHintEl) waitingHintEl.textContent = defaultWaitingHint;
1423
+ feedbackForm.classList.remove('hidden');
1424
+ submitBar.classList.remove('hidden');
1425
+ summaryCard.classList.remove('hidden');
1426
+ splitter.classList.remove('hidden');
1427
+ queueCard.classList.add('hidden');
1428
+ updateKeyModeUI();
1429
+ }
1430
+
1431
+ function renderQueueList() {
1432
+ if (!queueCard) return;
1433
+ const items = queueItems || [];
1434
+ if (!queueMode || items.length === 0) {
1435
+ queueCard.classList.add('hidden');
1436
+ return;
1437
+ }
1438
+ queueCard.classList.remove('hidden');
1439
+ if (queueCount) queueCount.textContent = String(items.length);
1440
+ queueList.innerHTML = '';
1441
+ items.forEach((it) => {
1442
+ const row = document.createElement('div');
1443
+ row.className = 'queue-item';
1444
+
1445
+ const src = document.createElement('span');
1446
+ src.className = 'queue-item__source' + (it.source === 'feishu' ? ' is-feishu' : '');
1447
+ src.textContent = it.source === 'feishu' ? (i18n.sourceFeishu || 'Feishu') : (i18n.sourcePanel || 'Panel');
1448
+ row.appendChild(src);
1449
+
1450
+ const text = document.createElement('span');
1451
+ text.className = 'queue-item__text';
1452
+ text.textContent = (it.text || '').replace(/\s+/g, ' ').trim();
1453
+ text.title = it.text || '';
1454
+ row.appendChild(text);
1455
+
1456
+ const metaBits = [];
1457
+ if (it.images) metaBits.push((i18n.queueMetaImages || 'img') + '×' + it.images);
1458
+ if (it.files) metaBits.push((i18n.queueMetaFiles || 'file') + '×' + it.files);
1459
+ if (metaBits.length) {
1460
+ const meta = document.createElement('span');
1461
+ meta.className = 'queue-item__meta';
1462
+ meta.textContent = metaBits.join(' ');
1463
+ row.appendChild(meta);
1464
+ }
1465
+
1466
+ const time = document.createElement('span');
1467
+ time.className = 'queue-item__time';
1468
+ time.textContent = formatHistoryTime(it.at);
1469
+ row.appendChild(time);
1470
+
1471
+ // 撤回:乐观移除本地列表(失败时下一秒轮询会把消息补回来),请求按队列项端口路由
1472
+ row.appendChild(makeRemoveBtn((ev) => {
1473
+ ev.stopPropagation();
1474
+ queueItems = queueItems.filter((x) => x.id !== it.id);
1475
+ renderQueueList();
1476
+ vscode.postMessage({ type: 'removeQueued', payload: { id: it.id, port: it.port } });
1477
+ }, 'queue-item__remove'));
1478
+
1479
+ queueList.appendChild(row);
1480
+ });
1481
+ }
1482
+
1122
1483
  // ---------- Messages from extension ----------
1123
1484
  window.addEventListener('message', event => {
1124
1485
  const message = event.data;
1125
1486
 
1126
1487
  switch (message.type) {
1127
1488
  case 'showFeedbackRequest':
1128
- waitingStatus.classList.add('hidden');
1129
- feedbackForm.classList.remove('hidden');
1130
- submitBar.classList.remove('hidden');
1489
+ enterFeedbackMode();
1131
1490
  if (submitFallbackTimer) { clearTimeout(submitFallbackTimer); submitFallbackTimer = null; }
1132
1491
  setSubmitting(false);
1133
1492
  currentRequestId = message.payload.requestId;
@@ -1138,14 +1497,15 @@
1138
1497
  cdRemainingMs = Math.max(0, requestTimeout * 1000 - (Date.now() - requestTimestamp));
1139
1498
  cdAnchor = Date.now();
1140
1499
  updatePauseUI();
1141
- summaryContent.innerHTML = renderMarkdown(message.payload.summary);
1142
- highlightCodeBlocks(summaryContent);
1143
- summaryContent.scrollTop = 0;
1500
+ // 存入历史并渲染最新一轮(含导航状态刷新)
1501
+ pushSummaryHistory(message.payload.summary);
1502
+ renderSummaryAt(0);
1144
1503
  projectInfo.textContent = message.payload.projectDir;
1145
1504
  projectInfo.title = message.payload.projectDir;
1146
1505
  updateCharCount();
1147
1506
  feedbackInput.focus();
1148
1507
  timeoutWrap.hidden = false;
1508
+ timeoutWrap.classList.remove('countdown--idle');
1149
1509
  if (countdownInterval) clearInterval(countdownInterval);
1150
1510
  updateCountdown();
1151
1511
  countdownInterval = setInterval(updateCountdown, 1000);
@@ -1156,12 +1516,10 @@
1156
1516
  if (submitFallbackTimer) { clearTimeout(submitFallbackTimer); submitFallbackTimer = null; }
1157
1517
  resetForm();
1158
1518
  setSubmitting(false);
1159
- feedbackForm.classList.add('hidden');
1160
- submitBar.classList.add('hidden');
1161
- waitingStatus.classList.remove('hidden');
1162
- timeoutWrap.hidden = true;
1163
1519
  cdPaused = false;
1164
1520
  updatePauseUI();
1521
+ // 等待态 = 排队模式:输入框保持可用,消息排队等 AI 下一轮读取
1522
+ enterQueueMode();
1165
1523
  break;
1166
1524
 
1167
1525
  case 'serverStatus':
@@ -1255,6 +1613,10 @@
1255
1613
  updateFeishuUI(message.payload);
1256
1614
  break;
1257
1615
 
1616
+ case 'feishuRegisterState':
1617
+ handleRegisterState(message.payload);
1618
+ break;
1619
+
1258
1620
  case 'feedbackSubmitted':
1259
1621
  // 提交已被 server 确认:写入历史 + 轻提示(queued = 撞上超时空窗、暂存待下一轮送达)
1260
1622
  pushHistory(pendingHistoryText);
@@ -1264,6 +1626,32 @@
1264
1626
  : (i18n.toastSubmitted || 'Feedback sent'));
1265
1627
  break;
1266
1628
 
1629
+ case 'queueSubmitted': {
1630
+ // 排队发送的结果回执:成功则清空输入并入历史;失败明确提示原因,内容保留可重试
1631
+ if (submitFallbackTimer) { clearTimeout(submitFallbackTimer); submitFallbackTimer = null; }
1632
+ setSubmitting(false);
1633
+ const p = message.payload || {};
1634
+ if (p.success) {
1635
+ pushHistory(pendingHistoryText);
1636
+ pendingHistoryText = '';
1637
+ clearComposer();
1638
+ showToast(i18n.toastPanelQueued || '✓ Queued — delivered when the AI finishes this task');
1639
+ } else {
1640
+ showToast(p.reason === 'pending'
1641
+ ? (i18n.queueFailedPending || 'AI is waiting for your feedback — submit it directly instead')
1642
+ : p.reason === 'disabled'
1643
+ ? (i18n.queueFailedDisabled || 'Busy-time queuing is turned off in settings')
1644
+ : (i18n.queueFailedNoServer || 'No active AI session found — the message was not queued'));
1645
+ }
1646
+ break;
1647
+ }
1648
+
1649
+ case 'queueState':
1650
+ // server 每秒随轮询下发的队列快照(extension 已做签名去重)
1651
+ queueItems = (message.payload && message.payload.items) || [];
1652
+ renderQueueList();
1653
+ break;
1654
+
1267
1655
  case 'toast':
1268
1656
  // extension 侧的通用轻提示(如暂停失败:请求已结束)
1269
1657
  if (message.payload && message.payload.text) showToast(message.payload.text);
@@ -1308,6 +1696,9 @@
1308
1696
  })();
1309
1697
 
1310
1698
  updateCharCount();
1699
+ // 默认进入排队模式(无等待中的请求时输入框即可用);若有请求,
1700
+ // extension 在收到 ready 后会推 showFeedbackRequest 切回反馈模式
1701
+ enterQueueMode();
1311
1702
  setInterval(() => vscode.postMessage({ type: 'checkServer' }), 5000);
1312
1703
  vscode.postMessage({ type: 'ready' });
1313
1704
  vscode.postMessage({ type: 'checkServer' });