cursor-feedback 2.2.0 → 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.
- package/CHANGELOG.md +7 -0
- package/dist/extension.js +228 -18
- package/dist/feishu.js +113 -0
- package/dist/i18n/en.json +23 -3
- package/dist/i18n/index.js +23 -1
- package/dist/i18n/zh-CN.json +23 -3
- package/dist/mcp-server.js +153 -15
- package/dist/webview/index.html +40 -12
- package/dist/webview/script.js +409 -28
- package/dist/webview/styles.css +215 -19
- package/package.json +4 -1
package/dist/webview/script.js
CHANGED
|
@@ -92,27 +92,40 @@
|
|
|
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');
|
|
101
|
-
const
|
|
100
|
+
const queueWhenBusyToggle = document.getElementById('queueWhenBusyToggle');
|
|
102
101
|
const osNotifySub = document.getElementById('osNotifySub');
|
|
103
102
|
const feishuAckSub = document.getElementById('feishuAckSub');
|
|
104
|
-
const feishuQueueSub = document.getElementById('feishuQueueSub');
|
|
105
103
|
const notifyTestBtn = document.getElementById('notifyTestBtn');
|
|
106
104
|
const notifyTestHint = document.getElementById('notifyTestHint');
|
|
107
105
|
const historyBtn = document.getElementById('historyBtn');
|
|
108
106
|
const historyPopup = document.getElementById('historyPopup');
|
|
109
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');
|
|
110
117
|
|
|
111
118
|
let uploadedImages = [];
|
|
112
119
|
let attachedFiles = [];
|
|
113
120
|
let codeRefs = [];
|
|
114
121
|
let currentRequestId = '';
|
|
115
122
|
let currentProjectDir = '';
|
|
123
|
+
// 排队模式:AI 正忙(无等待中的反馈请求)时,同一个输入框用于「排队发送」,
|
|
124
|
+
// 消息进服务端忙时队列,与飞书消息同队列按序送达
|
|
125
|
+
let queueMode = false;
|
|
126
|
+
let queueItems = [];
|
|
127
|
+
// 忙时排队全局开关(真相在 server,随 feishuState 同步):关了则等待态回到纯等待、不提供排队输入
|
|
128
|
+
let queueEnabled = true;
|
|
116
129
|
let requestTimestamp = 0;
|
|
117
130
|
let requestTimeout = 300;
|
|
118
131
|
let countdownInterval = null;
|
|
@@ -198,6 +211,8 @@
|
|
|
198
211
|
function closeFeishuModal() {
|
|
199
212
|
// 兜底:Esc 关闭等场景输入框可能不触发 blur,关闭前再存一次(已去重,不会重复提交)
|
|
200
213
|
saveFeishuConfigFromInputs();
|
|
214
|
+
// 扫码创建流程进行中:关闭弹窗即取消(server 侧 abort,二维码作废)
|
|
215
|
+
resetRegisterPanel(true);
|
|
201
216
|
feishuModal.classList.add('hidden');
|
|
202
217
|
feishuModal.setAttribute('aria-hidden', 'true');
|
|
203
218
|
}
|
|
@@ -212,10 +227,82 @@
|
|
|
212
227
|
closeFeishuModal();
|
|
213
228
|
}
|
|
214
229
|
});
|
|
215
|
-
|
|
216
|
-
|
|
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 } });
|
|
217
273
|
});
|
|
218
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
|
+
|
|
219
306
|
// 密钥显示/隐藏切换(小眼睛)
|
|
220
307
|
feishuSecretToggle.addEventListener('click', () => {
|
|
221
308
|
const show = feishuAppSecretInput.type === 'password';
|
|
@@ -241,8 +328,11 @@
|
|
|
241
328
|
feishuAckToggle.addEventListener('change', () => {
|
|
242
329
|
vscode.postMessage({ type: 'toggleFeishuAck', payload: { enabled: feishuAckToggle.checked } });
|
|
243
330
|
});
|
|
244
|
-
|
|
245
|
-
|
|
331
|
+
// 忙时排队是全局开关(飞书 + 面板共用队列),不受飞书主开关约束
|
|
332
|
+
queueWhenBusyToggle.addEventListener('change', () => {
|
|
333
|
+
vscode.postMessage({ type: 'toggleFeishuQueue', payload: { enabled: queueWhenBusyToggle.checked } });
|
|
334
|
+
queueEnabled = queueWhenBusyToggle.checked;
|
|
335
|
+
if (queueMode) enterQueueMode(); // 立即按新开关刷新等待态形态
|
|
246
336
|
});
|
|
247
337
|
|
|
248
338
|
// 发送测试通知:一键验证系统通知链路通不通(权限被拒时收不到,配合上方排查提示定位)
|
|
@@ -262,8 +352,6 @@
|
|
|
262
352
|
const feishuOn = feishuEnabledToggle.checked;
|
|
263
353
|
feishuAckToggle.disabled = !feishuOn;
|
|
264
354
|
if (feishuAckSub) feishuAckSub.classList.toggle('is-disabled', !feishuOn);
|
|
265
|
-
feishuQueueToggle.disabled = !feishuOn;
|
|
266
|
-
if (feishuQueueSub) feishuQueueSub.classList.toggle('is-disabled', !feishuOn);
|
|
267
355
|
}
|
|
268
356
|
|
|
269
357
|
function updateFeishuUI(state) {
|
|
@@ -284,7 +372,11 @@
|
|
|
284
372
|
feishuAckToggle.checked = state.feishuAck;
|
|
285
373
|
}
|
|
286
374
|
if (typeof state.feishuQueue === 'boolean') {
|
|
287
|
-
|
|
375
|
+
queueWhenBusyToggle.checked = state.feishuQueue;
|
|
376
|
+
if (state.feishuQueue !== queueEnabled) {
|
|
377
|
+
queueEnabled = state.feishuQueue;
|
|
378
|
+
if (queueMode) enterQueueMode(); // 开关在别的窗口/server 侧变化:同步刷新等待态形态
|
|
379
|
+
}
|
|
288
380
|
}
|
|
289
381
|
syncSubSwitchDisabled();
|
|
290
382
|
feishuStatusEl.classList.toggle('is-configured', !!state.configured && !state.bound);
|
|
@@ -313,7 +405,9 @@
|
|
|
313
405
|
let enterToSubmit = localStorage.getItem('cursorFeedback_enterToSubmit') === 'true';
|
|
314
406
|
|
|
315
407
|
function updateKeyModeUI() {
|
|
316
|
-
submitLabel.textContent =
|
|
408
|
+
submitLabel.textContent = queueMode
|
|
409
|
+
? (i18n.queueSend || 'Queue Message')
|
|
410
|
+
: (i18n.submitFeedback || 'Submit');
|
|
317
411
|
if (enterToSubmit) {
|
|
318
412
|
if (submitKbd) submitKbd.textContent = i18n.submitHintEnter || 'Enter';
|
|
319
413
|
toggleKeyModeBtn.classList.add('enter-mode');
|
|
@@ -711,6 +805,24 @@
|
|
|
711
805
|
}
|
|
712
806
|
restoreSummaryHeight();
|
|
713
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
|
+
|
|
714
826
|
let splitDragging = false;
|
|
715
827
|
let splitStartY = 0;
|
|
716
828
|
let splitStartH = 0;
|
|
@@ -718,7 +830,7 @@
|
|
|
718
830
|
splitter.addEventListener('mousedown', (e) => {
|
|
719
831
|
splitDragging = true;
|
|
720
832
|
splitStartY = e.clientY;
|
|
721
|
-
splitStartH = summaryCard.getBoundingClientRect().height;
|
|
833
|
+
splitStartH = (queueMode ? waitingStatus : summaryCard).getBoundingClientRect().height;
|
|
722
834
|
splitter.classList.add('is-dragging');
|
|
723
835
|
document.body.style.userSelect = 'none';
|
|
724
836
|
document.body.style.cursor = 'row-resize';
|
|
@@ -726,7 +838,9 @@
|
|
|
726
838
|
});
|
|
727
839
|
window.addEventListener('mousemove', (e) => {
|
|
728
840
|
if (!splitDragging) return;
|
|
729
|
-
|
|
841
|
+
const next = splitStartH + (e.clientY - splitStartY);
|
|
842
|
+
if (queueMode) applyQueueTopHeight(next);
|
|
843
|
+
else applySummaryHeight(next);
|
|
730
844
|
});
|
|
731
845
|
window.addEventListener('mouseup', () => {
|
|
732
846
|
if (!splitDragging) return;
|
|
@@ -734,13 +848,20 @@
|
|
|
734
848
|
splitter.classList.remove('is-dragging');
|
|
735
849
|
document.body.style.userSelect = '';
|
|
736
850
|
document.body.style.cursor = '';
|
|
737
|
-
|
|
851
|
+
// 两种形态共用同一比例:任一形态下拖动都会同步到另一形态
|
|
852
|
+
const el = queueMode ? waitingStatus : summaryCard;
|
|
853
|
+
const ratio = el.getBoundingClientRect().height / window.innerHeight;
|
|
738
854
|
localStorage.setItem(SUMMARY_RATIO_KEY, ratio.toFixed(4));
|
|
739
855
|
});
|
|
740
856
|
// 切换侧边栏 / 窗口缩放后用保存值恢复;不能读瞬时 getBoundingClientRect(隐藏态布局会把摘要异常压扁)
|
|
741
|
-
window.addEventListener('resize',
|
|
857
|
+
window.addEventListener('resize', () => {
|
|
858
|
+
restoreSummaryHeight();
|
|
859
|
+
if (queueMode) restoreQueueTopHeight();
|
|
860
|
+
});
|
|
742
861
|
document.addEventListener('visibilitychange', () => {
|
|
743
|
-
if (
|
|
862
|
+
if (document.hidden) return;
|
|
863
|
+
restoreSummaryHeight();
|
|
864
|
+
if (queueMode) restoreQueueTopHeight();
|
|
744
865
|
});
|
|
745
866
|
|
|
746
867
|
// ---------- Quick replies (一键发送的快捷短语,可编辑,存 localStorage) ----------
|
|
@@ -772,8 +893,10 @@
|
|
|
772
893
|
}
|
|
773
894
|
|
|
774
895
|
// 点击快捷短语=立即提交。已有草稿时把短语追加到末尾一起发,不悄悄丢弃用户已输入的内容
|
|
896
|
+
// (排队模式下同样可用:短语走排队发送)
|
|
775
897
|
function sendQuickReply(phrase) {
|
|
776
|
-
if (
|
|
898
|
+
if (submitBtn.disabled) return;
|
|
899
|
+
if (!currentRequestId && !queueMode) return;
|
|
777
900
|
const cur = feedbackInput.value.trim();
|
|
778
901
|
feedbackInput.value = cur ? cur + '\n' + phrase : phrase;
|
|
779
902
|
saveDraft();
|
|
@@ -872,6 +995,52 @@
|
|
|
872
995
|
}
|
|
873
996
|
renderQuickReplies();
|
|
874
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
|
+
|
|
875
1044
|
// ---------- Toast (提交结果轻提示) ----------
|
|
876
1045
|
let toastTimer = null;
|
|
877
1046
|
function showToast(text) {
|
|
@@ -1091,7 +1260,12 @@
|
|
|
1091
1260
|
}
|
|
1092
1261
|
|
|
1093
1262
|
function submitFeedback() {
|
|
1094
|
-
if (
|
|
1263
|
+
if (submitBtn.disabled) return;
|
|
1264
|
+
// 无等待中的请求:排队模式下改走「排队发送」,消息进服务端忙时队列
|
|
1265
|
+
if (!currentRequestId) {
|
|
1266
|
+
if (queueMode) queueSend();
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1095
1269
|
|
|
1096
1270
|
setSubmitting(true);
|
|
1097
1271
|
// 先暂存本次文本,等 extension 确认成功后写入历史(提交失败不入历史)
|
|
@@ -1112,6 +1286,38 @@
|
|
|
1112
1286
|
submitFallbackTimer = setTimeout(() => setSubmitting(false), 8000);
|
|
1113
1287
|
}
|
|
1114
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
|
+
|
|
1115
1321
|
submitBtn.addEventListener('click', submitFeedback);
|
|
1116
1322
|
feedbackInput.addEventListener('keydown', (e) => {
|
|
1117
1323
|
if (mentionOpen && !mentionPopup.classList.contains('hidden')) {
|
|
@@ -1129,15 +1335,158 @@
|
|
|
1129
1335
|
}
|
|
1130
1336
|
});
|
|
1131
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
|
+
|
|
1132
1483
|
// ---------- Messages from extension ----------
|
|
1133
1484
|
window.addEventListener('message', event => {
|
|
1134
1485
|
const message = event.data;
|
|
1135
1486
|
|
|
1136
1487
|
switch (message.type) {
|
|
1137
1488
|
case 'showFeedbackRequest':
|
|
1138
|
-
|
|
1139
|
-
feedbackForm.classList.remove('hidden');
|
|
1140
|
-
submitBar.classList.remove('hidden');
|
|
1489
|
+
enterFeedbackMode();
|
|
1141
1490
|
if (submitFallbackTimer) { clearTimeout(submitFallbackTimer); submitFallbackTimer = null; }
|
|
1142
1491
|
setSubmitting(false);
|
|
1143
1492
|
currentRequestId = message.payload.requestId;
|
|
@@ -1148,14 +1497,15 @@
|
|
|
1148
1497
|
cdRemainingMs = Math.max(0, requestTimeout * 1000 - (Date.now() - requestTimestamp));
|
|
1149
1498
|
cdAnchor = Date.now();
|
|
1150
1499
|
updatePauseUI();
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1500
|
+
// 存入历史并渲染最新一轮(含导航状态刷新)
|
|
1501
|
+
pushSummaryHistory(message.payload.summary);
|
|
1502
|
+
renderSummaryAt(0);
|
|
1154
1503
|
projectInfo.textContent = message.payload.projectDir;
|
|
1155
1504
|
projectInfo.title = message.payload.projectDir;
|
|
1156
1505
|
updateCharCount();
|
|
1157
1506
|
feedbackInput.focus();
|
|
1158
1507
|
timeoutWrap.hidden = false;
|
|
1508
|
+
timeoutWrap.classList.remove('countdown--idle');
|
|
1159
1509
|
if (countdownInterval) clearInterval(countdownInterval);
|
|
1160
1510
|
updateCountdown();
|
|
1161
1511
|
countdownInterval = setInterval(updateCountdown, 1000);
|
|
@@ -1166,12 +1516,10 @@
|
|
|
1166
1516
|
if (submitFallbackTimer) { clearTimeout(submitFallbackTimer); submitFallbackTimer = null; }
|
|
1167
1517
|
resetForm();
|
|
1168
1518
|
setSubmitting(false);
|
|
1169
|
-
feedbackForm.classList.add('hidden');
|
|
1170
|
-
submitBar.classList.add('hidden');
|
|
1171
|
-
waitingStatus.classList.remove('hidden');
|
|
1172
|
-
timeoutWrap.hidden = true;
|
|
1173
1519
|
cdPaused = false;
|
|
1174
1520
|
updatePauseUI();
|
|
1521
|
+
// 等待态 = 排队模式:输入框保持可用,消息排队等 AI 下一轮读取
|
|
1522
|
+
enterQueueMode();
|
|
1175
1523
|
break;
|
|
1176
1524
|
|
|
1177
1525
|
case 'serverStatus':
|
|
@@ -1265,6 +1613,10 @@
|
|
|
1265
1613
|
updateFeishuUI(message.payload);
|
|
1266
1614
|
break;
|
|
1267
1615
|
|
|
1616
|
+
case 'feishuRegisterState':
|
|
1617
|
+
handleRegisterState(message.payload);
|
|
1618
|
+
break;
|
|
1619
|
+
|
|
1268
1620
|
case 'feedbackSubmitted':
|
|
1269
1621
|
// 提交已被 server 确认:写入历史 + 轻提示(queued = 撞上超时空窗、暂存待下一轮送达)
|
|
1270
1622
|
pushHistory(pendingHistoryText);
|
|
@@ -1274,6 +1626,32 @@
|
|
|
1274
1626
|
: (i18n.toastSubmitted || 'Feedback sent'));
|
|
1275
1627
|
break;
|
|
1276
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
|
+
|
|
1277
1655
|
case 'toast':
|
|
1278
1656
|
// extension 侧的通用轻提示(如暂停失败:请求已结束)
|
|
1279
1657
|
if (message.payload && message.payload.text) showToast(message.payload.text);
|
|
@@ -1318,6 +1696,9 @@
|
|
|
1318
1696
|
})();
|
|
1319
1697
|
|
|
1320
1698
|
updateCharCount();
|
|
1699
|
+
// 默认进入排队模式(无等待中的请求时输入框即可用);若有请求,
|
|
1700
|
+
// extension 在收到 ready 后会推 showFeedbackRequest 切回反馈模式
|
|
1701
|
+
enterQueueMode();
|
|
1321
1702
|
setInterval(() => vscode.postMessage({ type: 'checkServer' }), 5000);
|
|
1322
1703
|
vscode.postMessage({ type: 'ready' });
|
|
1323
1704
|
vscode.postMessage({ type: 'checkServer' });
|