openclacky 1.5.20 → 1.5.21
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +18 -0
- data/lib/clacky/agent/chunk_index.rb +8 -1
- data/lib/clacky/agent/feedback.rb +30 -0
- data/lib/clacky/agent/history_navigation.rb +17 -3
- data/lib/clacky/agent/hook_manager.rb +6 -1
- data/lib/clacky/agent/llm_caller.rb +1 -0
- data/lib/clacky/agent/message_compressor_helper.rb +28 -2
- data/lib/clacky/agent/session_serializer.rb +20 -2
- data/lib/clacky/agent.rb +56 -43
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +5 -1
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +3 -2
- data/lib/clacky/client.rb +5 -5
- data/lib/clacky/deepseek_web_tools.rb +156 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +21 -1
- data/lib/clacky/extension/loader.rb +5 -3
- data/lib/clacky/locales/en.rb +5 -5
- data/lib/clacky/locales/zh.rb +5 -5
- data/lib/clacky/message_format/anthropic.rb +20 -0
- data/lib/clacky/message_format/open_ai.rb +31 -14
- data/lib/clacky/message_history.rb +21 -3
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +23 -14
- data/lib/clacky/search_config.rb +47 -13
- data/lib/clacky/server/browser_manager.rb +35 -5
- data/lib/clacky/server/channel/channel_manager.rb +6 -6
- data/lib/clacky/server/discover.rb +5 -4
- data/lib/clacky/server/http_server.rb +194 -47
- data/lib/clacky/server/model_prices.rb +5 -3
- data/lib/clacky/server/pid_files.rb +25 -0
- data/lib/clacky/server/server_master.rb +9 -3
- data/lib/clacky/server/session_registry.rb +19 -7
- data/lib/clacky/server/web_ui_controller.rb +61 -38
- data/lib/clacky/session_manager.rb +7 -1
- data/lib/clacky/skill.rb +1 -1
- data/lib/clacky/tools/ask_user.rb +4 -1
- data/lib/clacky/tools/base.rb +3 -2
- data/lib/clacky/tools/browser.rb +18 -2
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/terminal/session_manager.rb +19 -1
- data/lib/clacky/tools/terminal.rb +65 -23
- data/lib/clacky/tools/trash_manager.rb +5 -3
- data/lib/clacky/tools/web_fetch.rb +81 -22
- data/lib/clacky/tools/web_search.rb +45 -4
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +4 -3
- data/lib/clacky/utils/file_processor.rb +51 -0
- data/lib/clacky/utils/model_pricing.rb +132 -13
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +366 -105
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +3 -1
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/features/archive/store.js +30 -16
- data/lib/clacky/web/features/archive/view.js +20 -10
- data/lib/clacky/web/features/backup/view.js +5 -5
- data/lib/clacky/web/features/new-session/store.js +3 -3
- data/lib/clacky/web/features/trash/store.js +10 -7
- data/lib/clacky/web/features/trash/view.js +9 -4
- data/lib/clacky/web/i18n.js +106 -30
- data/lib/clacky/web/index.html +90 -60
- data/lib/clacky/web/projects.js +56 -1
- data/lib/clacky/web/sessions.js +244 -131
- data/lib/clacky/web/settings.js +143 -8
- data/lib/clacky/web/ws-dispatcher.js +9 -13
- data/lib/clacky.rb +1 -0
- metadata +4 -1
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -67,7 +67,7 @@ const Sessions = (() => {
|
|
|
67
67
|
const _groups = new Map(GROUP_SOURCES.map(source => [source, {
|
|
68
68
|
count: 0, // total sessions in this group (server-side, unpaginated)
|
|
69
69
|
latestUpdatedAt: null, // newest activity — decides the virtual entry's sort position
|
|
70
|
-
before: null, // cursor:
|
|
70
|
+
before: null, // cursor: server-issued sort key of the last row loaded into the sub-view
|
|
71
71
|
hasMore: false,
|
|
72
72
|
loadingMore: false,
|
|
73
73
|
}]));
|
|
@@ -87,6 +87,9 @@ const Sessions = (() => {
|
|
|
87
87
|
// Project-assigned sessions always render in the project section.
|
|
88
88
|
const _groupOf = s => (s && !s.project_id && _groups.get(s.source)) || null;
|
|
89
89
|
|
|
90
|
+
// Server-issued sort key of the last loaded main-list row — the loadMore cursor.
|
|
91
|
+
let _cursor = null;
|
|
92
|
+
|
|
90
93
|
// Merge server-reported group stats (REST/WS `groups` payload) into local state.
|
|
91
94
|
function _applyGroupStats(stats) {
|
|
92
95
|
if (!stats) return;
|
|
@@ -619,7 +622,6 @@ const Sessions = (() => {
|
|
|
619
622
|
try {
|
|
620
623
|
const session = await NewSessionStore.createSession({ existingSessions: _sessions, useDefaults: true });
|
|
621
624
|
if (!session) return;
|
|
622
|
-
NewSessionStore.reset();
|
|
623
625
|
// Add to local list immediately so renderList shows it and findOrFetch
|
|
624
626
|
// can locate it synchronously (without a round-trip to the API).
|
|
625
627
|
if (!_sessions.find(s => s.id === session.id)) {
|
|
@@ -980,6 +982,41 @@ const Sessions = (() => {
|
|
|
980
982
|
// ── sendMessage ────────────────────────────────────────────────────────
|
|
981
983
|
let _sending = false;
|
|
982
984
|
|
|
985
|
+
// Countdown state is per session; only the backend decides when to continue.
|
|
986
|
+
const _feedbackCountdowns = new Map();
|
|
987
|
+
function _renderFeedbackCountdown() {
|
|
988
|
+
const state = _feedbackCountdowns.get(Sessions.activeId);
|
|
989
|
+
document.querySelectorAll(".feedback-card:not(.feedback-card--submitted) .feedback-status").forEach(el => {
|
|
990
|
+
el.textContent = state?.deadline_ms != null
|
|
991
|
+
? I18n.t("chat.feedback_countdown", {
|
|
992
|
+
seconds: Math.max(0, Math.ceil((state.deadline_ms - Date.now()) / 1000))
|
|
993
|
+
})
|
|
994
|
+
: I18n.t("chat.feedback_waiting");
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function _hasPendingFeedback() {
|
|
999
|
+
return !!document.querySelector(".feedback-card:not(.feedback-card--submitted)");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function _refreshFeedbackStatus() {
|
|
1003
|
+
_renderFeedbackCountdown();
|
|
1004
|
+
const session = Sessions._lastSession;
|
|
1005
|
+
if (session && session.id === Sessions.activeId) {
|
|
1006
|
+
Sessions.updateStatusBar(session.status);
|
|
1007
|
+
Sessions.updateInfoBar(session);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function _interveneFeedbackCountdown() {
|
|
1012
|
+
const state = _feedbackCountdowns.get(Sessions.activeId);
|
|
1013
|
+
if (!state || state.intervened) return;
|
|
1014
|
+
state.intervened = true;
|
|
1015
|
+
state.deadline_ms = null;
|
|
1016
|
+
WS.send({ type: "feedback_intervene", session_id: Sessions.activeId, request_id: state.request_id });
|
|
1017
|
+
_renderFeedbackCountdown();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
983
1020
|
// ── ask_user feedback card ────────────────────────────────────────────
|
|
984
1021
|
//
|
|
985
1022
|
// The backend always sends a normalized `questions` array, but older stored
|
|
@@ -1012,31 +1049,7 @@ const Sessions = (() => {
|
|
|
1012
1049
|
}];
|
|
1013
1050
|
}
|
|
1014
1051
|
|
|
1015
|
-
function _feedbackNeedsCard(list) {
|
|
1016
|
-
return list.some(q => q.options.length > 0);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
function _feedbackPlainText(list, context) {
|
|
1020
|
-
// Normalize bullet symbols to markdown list format so marked renders them as <ul>
|
|
1021
|
-
const normalizeBullets = (t) => t ? t.replace(/^[•·‣▸▪\-–]\s*/gm, "- ") : t;
|
|
1022
|
-
const parts = [];
|
|
1023
|
-
if (context && context.trim()) parts.push(context.trim());
|
|
1024
|
-
list.forEach(q => {
|
|
1025
|
-
parts.push(q.question);
|
|
1026
|
-
if (q.description) parts.push(q.description);
|
|
1027
|
-
});
|
|
1028
|
-
return parts.map(normalizeBullets).join("\n\n");
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
// A single-choice question with no free-text escape submits on click, which
|
|
1032
|
-
// keeps the one-question case at one tap. Anything else needs an explicit
|
|
1033
|
-
// submit so partial answers aren't sent.
|
|
1034
|
-
function _feedbackIsInstant(list) {
|
|
1035
|
-
return list.length === 1 && !list[0].multi && !list[0].allow_free_text;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
1052
|
function _buildFeedbackCard(list, context, answered) {
|
|
1039
|
-
const instant = _feedbackIsInstant(list);
|
|
1040
1053
|
// Multiple open questions are walked one at a time. A submitted card is a
|
|
1041
1054
|
// read-only record, so it stacks instead — hiding past questions there
|
|
1042
1055
|
// would lose what was actually asked.
|
|
@@ -1071,7 +1084,7 @@ const Sessions = (() => {
|
|
|
1071
1084
|
}
|
|
1072
1085
|
html += `<div class="feedback-options">`;
|
|
1073
1086
|
q.options.forEach((opt, oi) => {
|
|
1074
|
-
const selected =
|
|
1087
|
+
const selected = q.recommended === oi ? " is-selected" : "";
|
|
1075
1088
|
const rec = q.recommended === oi
|
|
1076
1089
|
? `<span class="feedback-rec">${escapeHtml(I18n.t("chat.feedback_recommended"))}</span>` : "";
|
|
1077
1090
|
html += `<button class="feedback-option-btn${selected}" data-question-index="${qi}" data-option-index="${oi}"` +
|
|
@@ -1085,22 +1098,19 @@ const Sessions = (() => {
|
|
|
1085
1098
|
html += `</div>`;
|
|
1086
1099
|
});
|
|
1087
1100
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
`${escapeHtml(I18n.t("chat.feedback_next"))}</button>`;
|
|
1095
|
-
}
|
|
1096
|
-
html += `<button class="btn-primary feedback-submit-btn"${answered ? " disabled" : ""}` +
|
|
1097
|
-
`${stepped ? " hidden" : ""}>${escapeHtml(I18n.t("chat.feedback_submit"))}</button>`;
|
|
1098
|
-
html += `</div>`;
|
|
1101
|
+
html += `<div class="feedback-actions"><span class="feedback-status">${escapeHtml(I18n.t(answered ? "chat.feedback_answered" : "chat.feedback_waiting"))}</span>`;
|
|
1102
|
+
if (stepped) {
|
|
1103
|
+
html += `<button class="feedback-nav-btn feedback-back-btn" disabled>` +
|
|
1104
|
+
`${escapeHtml(I18n.t("chat.feedback_back"))}</button>` +
|
|
1105
|
+
`<button class="feedback-nav-btn feedback-next-btn">` +
|
|
1106
|
+
`${escapeHtml(I18n.t("chat.feedback_next"))}</button>`;
|
|
1099
1107
|
}
|
|
1100
|
-
html += `<
|
|
1108
|
+
html += `<button class="btn-primary feedback-submit-btn"${answered ? " disabled" : ""}` +
|
|
1109
|
+
`${stepped ? " hidden" : ""}>${escapeHtml(I18n.t("chat.feedback_submit"))}</button>`;
|
|
1110
|
+
html += `</div>`;
|
|
1101
1111
|
|
|
1102
1112
|
card.innerHTML = html;
|
|
1103
|
-
if (!answered) _wireFeedbackCard(card, list,
|
|
1113
|
+
if (!answered) _wireFeedbackCard(card, list, stepped);
|
|
1104
1114
|
return card;
|
|
1105
1115
|
}
|
|
1106
1116
|
|
|
@@ -1129,6 +1139,7 @@ const Sessions = (() => {
|
|
|
1129
1139
|
card.querySelectorAll(".feedback-option-btn, .feedback-submit-btn, .feedback-free-text, .feedback-nav-btn")
|
|
1130
1140
|
.forEach(el => { el.disabled = true; });
|
|
1131
1141
|
card.classList.add("feedback-card--submitted");
|
|
1142
|
+
card.querySelector(".feedback-status").textContent = I18n.t("chat.feedback_answered");
|
|
1132
1143
|
|
|
1133
1144
|
const input = $("user-input");
|
|
1134
1145
|
if (input) Composer.setText(input, lines.join("\n"));
|
|
@@ -1136,7 +1147,7 @@ const Sessions = (() => {
|
|
|
1136
1147
|
return true;
|
|
1137
1148
|
}
|
|
1138
1149
|
|
|
1139
|
-
function _wireFeedbackCard(card, list,
|
|
1150
|
+
function _wireFeedbackCard(card, list, stepped) {
|
|
1140
1151
|
let step = 0;
|
|
1141
1152
|
|
|
1142
1153
|
const showStep = (target) => {
|
|
@@ -1177,15 +1188,6 @@ const Sessions = (() => {
|
|
|
1177
1188
|
btn.onclick = () => {
|
|
1178
1189
|
const qi = Number(btn.dataset.questionIndex);
|
|
1179
1190
|
|
|
1180
|
-
if (instant) {
|
|
1181
|
-
card.querySelectorAll(".feedback-option-btn").forEach(b => { b.disabled = true; });
|
|
1182
|
-
card.classList.add("feedback-card--submitted");
|
|
1183
|
-
const input = $("user-input");
|
|
1184
|
-
if (input) Composer.setText(input, list[qi].options[Number(btn.dataset.optionIndex)]);
|
|
1185
|
-
_sendMessage();
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
1191
|
if (!list[qi].multi) {
|
|
1190
1192
|
card.querySelectorAll(`.feedback-option-btn[data-question-index="${qi}"]`)
|
|
1191
1193
|
.forEach(b => b.classList.remove("is-selected"));
|
|
@@ -1382,16 +1384,19 @@ const Sessions = (() => {
|
|
|
1382
1384
|
_imageSeq = 0;
|
|
1383
1385
|
_renderAttachmentPreviews();
|
|
1384
1386
|
|
|
1387
|
+
_interveneFeedbackCountdown();
|
|
1385
1388
|
WS.send({ type: "message", session_id: Sessions.activeId, content, files, references, lang: I18n.lang() });
|
|
1386
1389
|
|
|
1387
1390
|
// Disable any pending feedback cards — user has replied (either by clicking
|
|
1388
1391
|
// an option button or by typing directly). The backend has already consumed
|
|
1389
1392
|
// the feedback; make the frontend reflect that immediately.
|
|
1390
1393
|
document.querySelectorAll(".feedback-card:not(.feedback-card--submitted)").forEach(card => {
|
|
1391
|
-
card.querySelectorAll("
|
|
1394
|
+
card.querySelectorAll("button, input")
|
|
1392
1395
|
.forEach(el => { el.disabled = true; });
|
|
1393
1396
|
card.classList.add("feedback-card--submitted");
|
|
1397
|
+
card.querySelector(".feedback-status").textContent = I18n.t("chat.feedback_answered");
|
|
1394
1398
|
});
|
|
1399
|
+
_refreshFeedbackStatus();
|
|
1395
1400
|
|
|
1396
1401
|
Composer.clear(input);
|
|
1397
1402
|
if (typeof QuoteSelect !== "undefined") QuoteSelect.clear();
|
|
@@ -1420,6 +1425,64 @@ const Sessions = (() => {
|
|
|
1420
1425
|
}
|
|
1421
1426
|
// contenteditable composer: chip deletion, plain-text paste, Shift+Enter.
|
|
1422
1427
|
Composer.init($("user-input"));
|
|
1428
|
+
setInterval(_renderFeedbackCountdown, 250);
|
|
1429
|
+
document.addEventListener("input", (e) => {
|
|
1430
|
+
if (e.target.closest?.("#user-input, .feedback-free-text")) _interveneFeedbackCountdown();
|
|
1431
|
+
});
|
|
1432
|
+
document.addEventListener("click", (e) => {
|
|
1433
|
+
if (e.target.closest?.(".feedback-option-btn, .feedback-nav-btn, .feedback-dot")) {
|
|
1434
|
+
_interveneFeedbackCountdown();
|
|
1435
|
+
}
|
|
1436
|
+
}, true);
|
|
1437
|
+
|
|
1438
|
+
const modeButton = $("sib-mode");
|
|
1439
|
+
const modeMenu = $("sib-mode-menu");
|
|
1440
|
+
const closeModeMenu = () => {
|
|
1441
|
+
modeMenu.hidden = true;
|
|
1442
|
+
modeButton.setAttribute("aria-expanded", "false");
|
|
1443
|
+
modeButton.classList.remove("is-open");
|
|
1444
|
+
};
|
|
1445
|
+
modeButton?.addEventListener("click", () => {
|
|
1446
|
+
const open = modeMenu.hidden;
|
|
1447
|
+
modeMenu.hidden = !open;
|
|
1448
|
+
modeButton.setAttribute("aria-expanded", String(open));
|
|
1449
|
+
modeButton.classList.toggle("is-open", open);
|
|
1450
|
+
if (open) {
|
|
1451
|
+
const rect = modeButton.getBoundingClientRect();
|
|
1452
|
+
modeMenu.style.left = `${Math.max(8, Math.min(rect.left, window.innerWidth - 248))}px`;
|
|
1453
|
+
modeMenu.style.bottom = `${window.innerHeight - rect.top + 6}px`;
|
|
1454
|
+
modeMenu.querySelector('[aria-checked="true"]')?.focus();
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
document.addEventListener("click", (event) => {
|
|
1458
|
+
if (!event.target.closest("#sib-mode, #sib-mode-menu")) closeModeMenu();
|
|
1459
|
+
});
|
|
1460
|
+
modeMenu?.addEventListener("keydown", (event) => {
|
|
1461
|
+
if (event.key === "Escape") { closeModeMenu(); modeButton.focus(); }
|
|
1462
|
+
});
|
|
1463
|
+
modeMenu?.addEventListener("click", async (event) => {
|
|
1464
|
+
const option = event.target.closest("[data-permission-mode]");
|
|
1465
|
+
if (!option) return;
|
|
1466
|
+
const sessionId = Sessions.activeId;
|
|
1467
|
+
const mode = option.dataset.permissionMode;
|
|
1468
|
+
closeModeMenu();
|
|
1469
|
+
modeButton.disabled = true;
|
|
1470
|
+
try {
|
|
1471
|
+
const response = await fetch(`/api/sessions/${sessionId}/permission_mode`, {
|
|
1472
|
+
method: "PATCH",
|
|
1473
|
+
headers: { "Content-Type": "application/json" },
|
|
1474
|
+
body: JSON.stringify({ permission_mode: mode })
|
|
1475
|
+
});
|
|
1476
|
+
const data = await response.json();
|
|
1477
|
+
if (!response.ok) throw new Error(data.error || "Failed to switch permission mode");
|
|
1478
|
+
const session = Sessions.find(sessionId);
|
|
1479
|
+
if (session) session.permission_mode = data.permission_mode;
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
Modal.toast(error.message);
|
|
1482
|
+
} finally {
|
|
1483
|
+
Sessions.updateInfoBar(Sessions.find(Sessions.activeId));
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1423
1486
|
|
|
1424
1487
|
// Send & attach buttons
|
|
1425
1488
|
const behaviorMenu = $("input-behavior-menu");
|
|
@@ -2033,6 +2096,7 @@ const Sessions = (() => {
|
|
|
2033
2096
|
el.innerHTML = _buildUserBubbleHtml(ev);
|
|
2034
2097
|
if (ev.created_at) el.dataset.createdAt = ev.created_at;
|
|
2035
2098
|
if (ev.round_id) el.dataset.roundId = ev.round_id;
|
|
2099
|
+
if (ev.task_id) el.dataset.taskId = ev.task_id;
|
|
2036
2100
|
// Messages archived into a compressed chunk can't be edited (the backend
|
|
2037
2101
|
// truncate keys off the active in-memory history). Flag them so the edit
|
|
2038
2102
|
// affordance stays hidden.
|
|
@@ -2040,8 +2104,7 @@ const Sessions = (() => {
|
|
|
2040
2104
|
const wrap = document.createElement("div");
|
|
2041
2105
|
wrap.className = "msg-user-wrap";
|
|
2042
2106
|
wrap.appendChild(el);
|
|
2043
|
-
_appendUserActionBar(el, wrap);
|
|
2044
|
-
_appendMsgTime(wrap, ev.created_at);
|
|
2107
|
+
_appendMsgTime(_appendUserActionBar(el, wrap), ev.created_at);
|
|
2045
2108
|
outerContainer.appendChild(wrap);
|
|
2046
2109
|
break;
|
|
2047
2110
|
}
|
|
@@ -2172,18 +2235,6 @@ const Sessions = (() => {
|
|
|
2172
2235
|
const rfList = _normalizeFeedbackQuestions(ev.questions, ev.question, ev.options);
|
|
2173
2236
|
if (rfList.length === 0) break;
|
|
2174
2237
|
|
|
2175
|
-
if (!_feedbackNeedsCard(rfList)) {
|
|
2176
|
-
// Nothing selectable — render as plain assistant bubble
|
|
2177
|
-
const rfText = _feedbackPlainText(rfList, rfContext);
|
|
2178
|
-
const rfEl = document.createElement("div");
|
|
2179
|
-
rfEl.className = "msg msg-assistant";
|
|
2180
|
-
rfEl.dataset.raw = rfText;
|
|
2181
|
-
rfEl.innerHTML = _renderMarkdown(rfText);
|
|
2182
|
-
_appendCopyButton(rfEl);
|
|
2183
|
-
container.appendChild(rfEl);
|
|
2184
|
-
break;
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
2238
|
container.appendChild(_buildFeedbackCard(rfList, rfContext, ev._answered === true));
|
|
2188
2239
|
break;
|
|
2189
2240
|
}
|
|
@@ -2348,6 +2399,8 @@ const Sessions = (() => {
|
|
|
2348
2399
|
if (!prepend && !state.hasAfter) _flushPendingStdout();
|
|
2349
2400
|
}
|
|
2350
2401
|
|
|
2402
|
+
_refreshFeedbackStatus();
|
|
2403
|
+
|
|
2351
2404
|
// If no more history remains, insert a "beginning of conversation" marker at the top.
|
|
2352
2405
|
// Remove any existing marker first to avoid duplicates.
|
|
2353
2406
|
messages.querySelector(".history-start-marker")?.remove();
|
|
@@ -2450,10 +2503,13 @@ const Sessions = (() => {
|
|
|
2450
2503
|
const now = new Date();
|
|
2451
2504
|
const pad = n => String(n).padStart(2, "0");
|
|
2452
2505
|
const hhmm = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
d.
|
|
2456
|
-
|
|
2506
|
+
const sameYear = d.getFullYear() === now.getFullYear();
|
|
2507
|
+
const sameDay = sameYear &&
|
|
2508
|
+
d.getMonth() === now.getMonth() &&
|
|
2509
|
+
d.getDate() === now.getDate();
|
|
2510
|
+
if (sameDay) return hhmm;
|
|
2511
|
+
const md = `${d.getMonth() + 1}月${d.getDate()}日`;
|
|
2512
|
+
return sameYear ? `${md} ${hhmm}` : `${d.getFullYear()}年${md} ${hhmm}`;
|
|
2457
2513
|
}
|
|
2458
2514
|
|
|
2459
2515
|
// Append a .msg-time span to a message element.
|
|
@@ -2468,15 +2524,15 @@ const Sessions = (() => {
|
|
|
2468
2524
|
|
|
2469
2525
|
// ── User message action bar (copy + edit) ───────────────────────────────
|
|
2470
2526
|
|
|
2471
|
-
const COPY_SVG = `<svg class="msg-user-copy-icon" viewBox="0 0
|
|
2472
|
-
`<
|
|
2527
|
+
const COPY_SVG = `<svg class="msg-user-copy-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
|
|
2528
|
+
`<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>` +
|
|
2473
2529
|
`</svg>` +
|
|
2474
|
-
`<svg class="msg-user-copy-icon-check" viewBox="0 0
|
|
2475
|
-
`<path
|
|
2530
|
+
`<svg class="msg-user-copy-icon-check" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
|
|
2531
|
+
`<path d="M20 6 9 17l-5-5"/>` +
|
|
2476
2532
|
`</svg>`;
|
|
2477
2533
|
|
|
2478
|
-
const EDIT_SVG = `<svg viewBox="0 0
|
|
2479
|
-
`<path
|
|
2534
|
+
const EDIT_SVG = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
|
|
2535
|
+
`<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4z"/>` +
|
|
2480
2536
|
`</svg>`;
|
|
2481
2537
|
|
|
2482
2538
|
function _appendUserActionBar(el, wrap) {
|
|
@@ -2520,6 +2576,7 @@ const Sessions = (() => {
|
|
|
2520
2576
|
// chunk — editing them would silently no-op on the backend.
|
|
2521
2577
|
if (el.dataset.editable !== "false") bar.appendChild(editBtn);
|
|
2522
2578
|
wrap.appendChild(bar);
|
|
2579
|
+
return bar;
|
|
2523
2580
|
}
|
|
2524
2581
|
|
|
2525
2582
|
function _activeSessionIsRunning() {
|
|
@@ -2896,14 +2953,49 @@ const Sessions = (() => {
|
|
|
2896
2953
|
// visible in a single-line ellipsis layout. Backend already gives us a
|
|
2897
2954
|
// ±N byte window, but for ASCII-heavy lines that's still wider than the
|
|
2898
2955
|
// sidebar can render on one line. We trim the head when the match sits
|
|
2899
|
-
// too far to the right, keeping ~`headRoom` chars before it
|
|
2900
|
-
|
|
2901
|
-
|
|
2956
|
+
// too far to the right, keeping ~`headRoom` chars before it and snapping
|
|
2957
|
+
// forward to a word boundary so the head never starts mid-token.
|
|
2958
|
+
function _centerSnippet(text, query, headRoom = 28) {
|
|
2959
|
+
const t = (text || "").trim();
|
|
2902
2960
|
const q = (query || "").trim();
|
|
2903
2961
|
if (!q) return t;
|
|
2904
2962
|
const idx = t.toLowerCase().indexOf(q.toLowerCase());
|
|
2905
2963
|
if (idx <= headRoom) return t;
|
|
2906
|
-
|
|
2964
|
+
let from = idx - headRoom;
|
|
2965
|
+
const space = t.indexOf(" ", from);
|
|
2966
|
+
if (space !== -1 && space - from <= 12) from = space + 1;
|
|
2967
|
+
return "…" + t.slice(from).replace(/^…\s*/, "");
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
// Snippets are raw grep hits from session JSON / chunk markdown, so drop
|
|
2971
|
+
// JSON escapes plus the markdown syntax that reads as noise in a one-line
|
|
2972
|
+
// teaser (**bold**, `code`, [label](url), list markers, stray ellipses).
|
|
2973
|
+
function _cleanSnippet(text) {
|
|
2974
|
+
return (text || "")
|
|
2975
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
2976
|
+
.replace(/\\[nrt]/g, " ")
|
|
2977
|
+
.replace(/\\(["\\/])/g, "$1")
|
|
2978
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
2979
|
+
.replace(/(^|\s)[-*+>]\s+/g, "$1")
|
|
2980
|
+
.replace(/(^|\s)#{1,6}\s+/g, "$1")
|
|
2981
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
2982
|
+
.replace(/__([^_]+)__/g, "$1")
|
|
2983
|
+
.replace(/~~([^~]+)~~/g, "$1")
|
|
2984
|
+
// Orphan markers left behind when the grep window cuts a pair in half.
|
|
2985
|
+
.replace(/(?<![\w*])\*{2,}(?![\w*])/g, "")
|
|
2986
|
+
.replace(/`/g, "")
|
|
2987
|
+
.replace(/<\/?[a-zA-Z][^>]{0,40}>/g, " ")
|
|
2988
|
+
// Multi-byte chars that grep cut in half surface as consecutive "?".
|
|
2989
|
+
.replace(/\?{2,}/g, "…")
|
|
2990
|
+
// JSON key/value remnants when the hit lands inside a serialized field.
|
|
2991
|
+
.replace(/^…?\s*"?[^"]{0,24}"\s*:\s*/, "…")
|
|
2992
|
+
.replace(/…\s*["',,、。;;::\s]+/g, "…")
|
|
2993
|
+
.replace(/([\u4e00-\u9fff))】」])\?+(?=…|\.\.\.)/g, "$1…")
|
|
2994
|
+
.replace(/^[":;,\s]+|[";,\s]+$/g, "")
|
|
2995
|
+
.replace(/(?:…|\.{3,})+/g, "…")
|
|
2996
|
+
.replace(/([,。;:、!?)】」])\s+/g, "$1")
|
|
2997
|
+
.replace(/\s+/g, " ")
|
|
2998
|
+
.trim();
|
|
2907
2999
|
}
|
|
2908
3000
|
|
|
2909
3001
|
// Reveal a single-line overflowing label on hover. Callers provide their
|
|
@@ -3053,7 +3145,7 @@ const Sessions = (() => {
|
|
|
3053
3145
|
: "";
|
|
3054
3146
|
|
|
3055
3147
|
const snippetHtml = (s._matchVia === "content" && s.search_snippet)
|
|
3056
|
-
? `<div class="session-snippet">${_highlightSnippet(_centerSnippet(s.search_snippet, _filter.q), _filter.q)}</div>`
|
|
3148
|
+
? `<div class="session-snippet">${_highlightSnippet(_centerSnippet(_cleanSnippet(s.search_snippet), _filter.q), _filter.q)}</div>`
|
|
3057
3149
|
: "";
|
|
3058
3150
|
|
|
3059
3151
|
el.innerHTML = `
|
|
@@ -3316,9 +3408,10 @@ const Sessions = (() => {
|
|
|
3316
3408
|
document.addEventListener("click", async (e) => {
|
|
3317
3409
|
// Localhost dev-server URLs open in the preview panel instead of a new
|
|
3318
3410
|
// tab (local deployments only — the proxy can't reach a remote user's
|
|
3319
|
-
// localhost).
|
|
3411
|
+
// localhost). Links that belong to the platform (not a dev server) opt
|
|
3412
|
+
// out with data-open-external, so they still get a real browser tab.
|
|
3320
3413
|
const localLink = e.target.closest("a[href]");
|
|
3321
|
-
if (localLink) {
|
|
3414
|
+
if (localLink && !localLink.hasAttribute("data-open-external")) {
|
|
3322
3415
|
const href = localLink.getAttribute("href") || "";
|
|
3323
3416
|
if (/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?([/?#]|$)/i.test(href) &&
|
|
3324
3417
|
["localhost", "127.0.0.1", "::1"].includes(window.location.hostname) &&
|
|
@@ -3376,14 +3469,27 @@ const Sessions = (() => {
|
|
|
3376
3469
|
// ── List management ───────────────────────────────────────────────────
|
|
3377
3470
|
|
|
3378
3471
|
/** Populate list from initial session_list WS event (connect only). */
|
|
3379
|
-
setAll(list, hasMore = false, groups = {}) {
|
|
3472
|
+
setAll(list, hasMore = false, groups = {}, nextCursor = null) {
|
|
3380
3473
|
_sessions.length = 0;
|
|
3381
3474
|
_sessions.push(...list);
|
|
3382
3475
|
_hasMore = !!hasMore;
|
|
3476
|
+
_cursor = nextCursor;
|
|
3383
3477
|
_groups.forEach(g => { g.count = 0; g.latestUpdatedAt = null; });
|
|
3384
3478
|
_applyGroupStats(groups);
|
|
3385
3479
|
},
|
|
3386
3480
|
|
|
3481
|
+
/** Whether a full snapshot sorts inside the loaded pages of the main
|
|
3482
|
+
* list. Grouped sessions render behind their group entry instead. */
|
|
3483
|
+
belongsToLoadedPage(session) {
|
|
3484
|
+
if (_groupOf(session) || session.archived_at) return false;
|
|
3485
|
+
if (session.pinned || !_hasMore || !_cursor) return true;
|
|
3486
|
+
const t = session.updated_at || session.created_at;
|
|
3487
|
+
if (!t) return false;
|
|
3488
|
+
// Same-second ties fall back to the server's id order, like the cursor itself.
|
|
3489
|
+
const diff = new Date(t) - new Date(_cursor.before);
|
|
3490
|
+
return diff > 0 || (diff === 0 && session.id >= _cursor.before_id);
|
|
3491
|
+
},
|
|
3492
|
+
|
|
3387
3493
|
/** Insert or refresh a newly created session in the local list. */
|
|
3388
3494
|
add(session) {
|
|
3389
3495
|
if (!session || !session.id) return;
|
|
@@ -3475,6 +3581,7 @@ const Sessions = (() => {
|
|
|
3475
3581
|
}
|
|
3476
3582
|
|
|
3477
3583
|
const updated = s || _extraSessions.find(e => e.id === id);
|
|
3584
|
+
if (id === _activeId && "name" in fields) Sessions.updateChatHeader(updated);
|
|
3478
3585
|
const isGroup = _groupOf(updated);
|
|
3479
3586
|
if (wasGroup !== isGroup) {
|
|
3480
3587
|
if (wasGroup) wasGroup.count = Math.max(0, wasGroup.count - 1);
|
|
@@ -3567,19 +3674,8 @@ const Sessions = (() => {
|
|
|
3567
3674
|
Sessions.renderList();
|
|
3568
3675
|
|
|
3569
3676
|
try {
|
|
3570
|
-
// Cursor: oldest activity time of non-pinned, non-grouped, non-project
|
|
3571
|
-
// sessions. Project sessions sit in _sessions but render in their own
|
|
3572
|
-
// section; counting them here would drag the cursor back to a stale
|
|
3573
|
-
// timestamp and skip a whole page of regular sessions.
|
|
3574
|
-
const oldest = _sessions.reduce((min, s) => {
|
|
3575
|
-
if (s.pinned || s.project_id || _groupOf(s)) return min;
|
|
3576
|
-
const t = s.updated_at || s.created_at;
|
|
3577
|
-
if (!t) return min;
|
|
3578
|
-
return (!min || t < min) ? t : min;
|
|
3579
|
-
}, null);
|
|
3580
|
-
|
|
3581
3677
|
const params = new URLSearchParams({ limit: "10", exclude_type: GROUP_SOURCES.join(",") });
|
|
3582
|
-
if (
|
|
3678
|
+
if (_cursor) { params.set("before", _cursor.before); params.set("before_id", _cursor.before_id); }
|
|
3583
3679
|
if (_filter.q) params.set("q", _filter.q);
|
|
3584
3680
|
if (_filter.date) params.set("date", _filter.date);
|
|
3585
3681
|
if (_filter.type) params.set("type", _filter.type);
|
|
@@ -3592,6 +3688,7 @@ const Sessions = (() => {
|
|
|
3592
3688
|
if (!_sessions.find(x => x.id === s.id)) _sessions.push(s);
|
|
3593
3689
|
});
|
|
3594
3690
|
_hasMore = !!data.has_more;
|
|
3691
|
+
if (data.next_cursor) _cursor = data.next_cursor;
|
|
3595
3692
|
_applyGroupStats(data.groups);
|
|
3596
3693
|
} catch (e) {
|
|
3597
3694
|
console.error("loadMore error:", e);
|
|
@@ -3623,7 +3720,7 @@ const Sessions = (() => {
|
|
|
3623
3720
|
|
|
3624
3721
|
try {
|
|
3625
3722
|
const params = new URLSearchParams({ limit: "10", type: source });
|
|
3626
|
-
if (group.before) params.set("before", group.before);
|
|
3723
|
+
if (group.before) { params.set("before", group.before.before); params.set("before_id", group.before.before_id); }
|
|
3627
3724
|
|
|
3628
3725
|
const res = await fetch(`/api/sessions?${params}`);
|
|
3629
3726
|
if (!res.ok) return;
|
|
@@ -3633,16 +3730,7 @@ const Sessions = (() => {
|
|
|
3633
3730
|
rows.forEach(s => {
|
|
3634
3731
|
if (!_sessions.find(x => x.id === s.id)) _sessions.push(s);
|
|
3635
3732
|
});
|
|
3636
|
-
|
|
3637
|
-
// (exclude pinned — backend returns all pinned on the first page,
|
|
3638
|
-
// bypassing pagination, so their time must not drag the cursor back).
|
|
3639
|
-
const oldest = rows.reduce((min, s) => {
|
|
3640
|
-
if (s.pinned) return min;
|
|
3641
|
-
const t = s.updated_at || s.created_at;
|
|
3642
|
-
if (!t) return min;
|
|
3643
|
-
return (!min || t < min) ? t : min;
|
|
3644
|
-
}, null);
|
|
3645
|
-
if (oldest) group.before = oldest;
|
|
3733
|
+
if (data.next_cursor) group.before = data.next_cursor;
|
|
3646
3734
|
group.hasMore = !!data.has_more;
|
|
3647
3735
|
_applyGroupStats(data.groups);
|
|
3648
3736
|
} catch (e) {
|
|
@@ -4006,7 +4094,8 @@ const Sessions = (() => {
|
|
|
4006
4094
|
// Within same pinned status, sort by last activity (newest first)
|
|
4007
4095
|
const ta = a.updated_at || a.created_at;
|
|
4008
4096
|
const tb = b.updated_at || b.created_at;
|
|
4009
|
-
|
|
4097
|
+
// Same-second ties follow the server's id order so arrival order can't reshuffle rows.
|
|
4098
|
+
return (new Date(tb || 0) - new Date(ta || 0)) || (a.id < b.id ? 1 : a.id > b.id ? -1 : 0);
|
|
4010
4099
|
};
|
|
4011
4100
|
|
|
4012
4101
|
// ── Sidebar list always shows the full session set, sorted ───────
|
|
@@ -4458,6 +4547,7 @@ const Sessions = (() => {
|
|
|
4458
4547
|
Sessions.remove(sessionId);
|
|
4459
4548
|
Sessions.renderList();
|
|
4460
4549
|
Projects.renderSection();
|
|
4550
|
+
Trash.refreshIfShown();
|
|
4461
4551
|
// If deleted session was active, switch to welcome
|
|
4462
4552
|
if (sessionId === _activeId) {
|
|
4463
4553
|
Router.navigate("welcome");
|
|
@@ -4539,7 +4629,7 @@ const Sessions = (() => {
|
|
|
4539
4629
|
const inp = $("user-input");
|
|
4540
4630
|
if (inp) {
|
|
4541
4631
|
const mobile = window.innerWidth <= 768;
|
|
4542
|
-
const key = status === "running"
|
|
4632
|
+
const key = _hasPendingFeedback() ? "chat.input.placeholderFeedback" : status === "running"
|
|
4543
4633
|
? (mobile ? "chat.input.placeholderRunningMobile" : "chat.input.placeholderRunning")
|
|
4544
4634
|
: (mobile ? "chat.input.placeholderMobile" : "chat.input.placeholder");
|
|
4545
4635
|
inp.setAttribute("data-i18n-placeholder", key);
|
|
@@ -4569,10 +4659,10 @@ const Sessions = (() => {
|
|
|
4569
4659
|
if (!s) {
|
|
4570
4660
|
// Hide all spans when no session. Fields that own a static caret (dir,
|
|
4571
4661
|
// reasoning) keep their markup and only blank the text node.
|
|
4572
|
-
["sib-id", "sib-status", "sib-
|
|
4662
|
+
["sib-id", "sib-status", "sib-model", "sib-cost"].forEach(id => {
|
|
4573
4663
|
const el = $(id); if (el) el.textContent = "";
|
|
4574
4664
|
});
|
|
4575
|
-
["sib-dir-text", "sib-reasoning-text"].forEach(id => {
|
|
4665
|
+
["sib-dir-text", "sib-reasoning-text", "sib-mode-text"].forEach(id => {
|
|
4576
4666
|
const el = $(id); if (el) el.textContent = "";
|
|
4577
4667
|
});
|
|
4578
4668
|
const sibIdEl = $("sib-id");
|
|
@@ -4587,7 +4677,7 @@ const Sessions = (() => {
|
|
|
4587
4677
|
// Status dot + text — first
|
|
4588
4678
|
const sibStatus = $("sib-status");
|
|
4589
4679
|
if (sibStatus) {
|
|
4590
|
-
const st = s.status || "idle";
|
|
4680
|
+
const st = _hasPendingFeedback() ? "awaiting_feedback" : (s.status || "idle");
|
|
4591
4681
|
const labelKey = `sib.status.${st}`;
|
|
4592
4682
|
const label = I18n.t(labelKey);
|
|
4593
4683
|
sibStatus.innerHTML = `<span class="sib-dot"></span>${escapeHtml(label === labelKey ? st : label)}`;
|
|
@@ -4629,16 +4719,28 @@ const Sessions = (() => {
|
|
|
4629
4719
|
}
|
|
4630
4720
|
}
|
|
4631
4721
|
|
|
4632
|
-
// Permission mode —
|
|
4722
|
+
// Permission mode — ordinary Web sessions default to confirm_all.
|
|
4633
4723
|
const sibMode = $("sib-mode");
|
|
4724
|
+
const sibModeText = $("sib-mode-text");
|
|
4634
4725
|
const sibSepAfterMode = document.querySelector(".sib-sep-after-mode");
|
|
4635
|
-
if (sibMode) {
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4726
|
+
if (sibMode && sibModeText) {
|
|
4727
|
+
const mode = s.permission_mode || "confirm_all";
|
|
4728
|
+
sibModeText.textContent = I18n.t(`sib.mode.${mode}`);
|
|
4729
|
+
document.querySelectorAll("[data-permission-mode]").forEach(option => {
|
|
4730
|
+
option.setAttribute("aria-checked", String(option.dataset.permissionMode === mode));
|
|
4731
|
+
});
|
|
4732
|
+
if (sibMode.dataset.sessionId !== s.id) {
|
|
4733
|
+
$("sib-mode-menu").hidden = true;
|
|
4734
|
+
sibMode.classList.remove("is-open");
|
|
4735
|
+
sibMode.setAttribute("aria-expanded", "false");
|
|
4736
|
+
}
|
|
4737
|
+
sibMode.dataset.sessionId = s.id;
|
|
4738
|
+
sibMode.disabled = false;
|
|
4739
|
+
sibMode.title = I18n.t("sib.mode.tooltip");
|
|
4740
|
+
sibMode.style.display = "";
|
|
4639
4741
|
}
|
|
4640
4742
|
if (sibSepAfterMode) {
|
|
4641
|
-
sibSepAfterMode.style.display =
|
|
4743
|
+
sibSepAfterMode.style.display = "";
|
|
4642
4744
|
}
|
|
4643
4745
|
|
|
4644
4746
|
// Model — hide wrap entirely if empty
|
|
@@ -4678,7 +4780,6 @@ const Sessions = (() => {
|
|
|
4678
4780
|
|
|
4679
4781
|
const sibReasoning = $("sib-reasoning");
|
|
4680
4782
|
const sibReasoningWrap = $("sib-reasoning-wrap");
|
|
4681
|
-
const sibSepAfterReasoning = document.querySelector(".sib-sep-after-reasoning");
|
|
4682
4783
|
if (sibReasoning) {
|
|
4683
4784
|
const eff = (s.reasoning_effort || "off").toLowerCase();
|
|
4684
4785
|
const sibReasoningText = $("sib-reasoning-text");
|
|
@@ -4687,7 +4788,6 @@ const Sessions = (() => {
|
|
|
4687
4788
|
sibReasoning.dataset.reasoningEffort = eff;
|
|
4688
4789
|
}
|
|
4689
4790
|
if (sibReasoningWrap) sibReasoningWrap.style.display = "";
|
|
4690
|
-
if (sibSepAfterReasoning) sibSepAfterReasoning.style.display = "";
|
|
4691
4791
|
|
|
4692
4792
|
// Latency signal — read from s.latest_latency (populated by:
|
|
4693
4793
|
// - HTTP /api/sessions → session_registry#list (from agent.latest_latency)
|
|
@@ -5087,8 +5187,8 @@ const Sessions = (() => {
|
|
|
5087
5187
|
const wrap = document.createElement("div");
|
|
5088
5188
|
wrap.className = "msg-user-wrap";
|
|
5089
5189
|
wrap.appendChild(el);
|
|
5090
|
-
_appendUserActionBar(el, wrap);
|
|
5091
|
-
if (time) _appendMsgTime(
|
|
5190
|
+
const bar = _appendUserActionBar(el, wrap);
|
|
5191
|
+
if (time) _appendMsgTime(bar, time);
|
|
5092
5192
|
messages.appendChild(wrap);
|
|
5093
5193
|
_refreshEditButtons(messages);
|
|
5094
5194
|
} else {
|
|
@@ -5142,25 +5242,38 @@ const Sessions = (() => {
|
|
|
5142
5242
|
_scrollToBottomIfNeeded(messages);
|
|
5143
5243
|
},
|
|
5144
5244
|
|
|
5245
|
+
updateFeedbackCountdown(ev) {
|
|
5246
|
+
const previous = _feedbackCountdowns.get(ev.session_id);
|
|
5247
|
+
if (ev.resolved) {
|
|
5248
|
+
if (ev.request_id && previous?.request_id !== ev.request_id) return;
|
|
5249
|
+
_feedbackCountdowns.delete(ev.session_id);
|
|
5250
|
+
if (ev.session_id === Sessions.activeId && ev.resolution === "timeout") {
|
|
5251
|
+
document.querySelectorAll(".feedback-card:not(.feedback-card--submitted)").forEach(card => {
|
|
5252
|
+
card.classList.add("feedback-card--submitted");
|
|
5253
|
+
card.querySelector(".feedback-status").textContent = I18n.t("chat.feedback_timeout");
|
|
5254
|
+
card.querySelectorAll("button, input").forEach(el => { el.disabled = true; });
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
5257
|
+
} else {
|
|
5258
|
+
_feedbackCountdowns.set(ev.session_id, ev);
|
|
5259
|
+
if (ev.session_id === Sessions.activeId && Composer.text($("user-input")).trim()) {
|
|
5260
|
+
_interveneFeedbackCountdown();
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
_refreshFeedbackStatus();
|
|
5264
|
+
},
|
|
5265
|
+
|
|
5145
5266
|
// Display an ask_user UI card with clickable options.
|
|
5146
|
-
// Called when the agent needs user input to continue.
|
|
5147
5267
|
showFeedbackRequest(question, context, options, questions) {
|
|
5148
5268
|
Sessions.collapseToolGroup();
|
|
5149
5269
|
const messages = RenderTarget.current();
|
|
5150
5270
|
const list = _normalizeFeedbackQuestions(questions, question, options);
|
|
5151
5271
|
|
|
5152
|
-
|
|
5153
|
-
if (!_feedbackNeedsCard(list)) {
|
|
5154
|
-
const text = _feedbackPlainText(list, context);
|
|
5155
|
-
if (!text) return;
|
|
5156
|
-
// Pass raw markdown; appendMsg renders it via _renderMarkdown and
|
|
5157
|
-
// also stashes it on dataset.raw for the copy button.
|
|
5158
|
-
Sessions.appendMsg("assistant", text);
|
|
5159
|
-
return;
|
|
5160
|
-
}
|
|
5272
|
+
if (list.length === 0) return;
|
|
5161
5273
|
|
|
5162
5274
|
const card = _buildFeedbackCard(list, context, false);
|
|
5163
5275
|
messages.appendChild(card);
|
|
5276
|
+
_refreshFeedbackStatus();
|
|
5164
5277
|
_scrollToBottomIfNeeded(messages);
|
|
5165
5278
|
},
|
|
5166
5279
|
|