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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -0
- package/README_CN.md +1 -0
- package/dist/extension.js +250 -18
- package/dist/feishu.js +132 -2
- package/dist/i18n/en.json +23 -1
- package/dist/i18n/index.js +23 -1
- package/dist/i18n/zh-CN.json +23 -1
- package/dist/mcp-server.js +422 -13
- package/dist/webview/index.html +40 -4
- package/dist/webview/script.js +412 -21
- package/dist/webview/styles.css +215 -19
- package/package.json +4 -1
package/dist/webview/script.js
CHANGED
|
@@ -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
|
-
|
|
214
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
857
|
+
window.addEventListener('resize', () => {
|
|
858
|
+
restoreSummaryHeight();
|
|
859
|
+
if (queueMode) restoreQueueTopHeight();
|
|
860
|
+
});
|
|
732
861
|
document.addEventListener('visibilitychange', () => {
|
|
733
|
-
if (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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' });
|