viberadar 0.3.37 → 0.3.38
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/dist/ui/dashboard.html +246 -27
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -675,6 +675,41 @@
|
|
|
675
675
|
font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
|
|
676
676
|
border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
|
|
677
677
|
}
|
|
678
|
+
/* ── Console Tabs ───────────────────────────────────────────────────────── */
|
|
679
|
+
.agent-tabs-bar {
|
|
680
|
+
display: flex; align-items: stretch; overflow-x: auto;
|
|
681
|
+
background: #0a0e15; border-bottom: 1px solid var(--border);
|
|
682
|
+
flex-shrink: 0; min-height: 30px;
|
|
683
|
+
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
|
684
|
+
}
|
|
685
|
+
.agent-tabs-bar::-webkit-scrollbar { height: 3px; }
|
|
686
|
+
.agent-tabs-bar::-webkit-scrollbar-thumb { background: var(--border); }
|
|
687
|
+
.agent-tab {
|
|
688
|
+
display: flex; align-items: center; gap: 6px;
|
|
689
|
+
padding: 0 8px 0 10px; cursor: pointer;
|
|
690
|
+
white-space: nowrap; border-right: 1px solid var(--border);
|
|
691
|
+
font-size: 11px; color: var(--muted); background: transparent;
|
|
692
|
+
max-width: 200px; flex-shrink: 0; user-select: none;
|
|
693
|
+
transition: background 0.1s, color 0.1s;
|
|
694
|
+
border-bottom: 2px solid transparent;
|
|
695
|
+
}
|
|
696
|
+
.agent-tab:hover { background: var(--bg-hover); color: var(--text); }
|
|
697
|
+
.agent-tab.active { background: var(--bg-card); color: var(--text); border-bottom-color: var(--accent); }
|
|
698
|
+
.agent-tab-title { overflow: hidden; text-overflow: ellipsis; max-width: 140px; }
|
|
699
|
+
.agent-tab-close {
|
|
700
|
+
background: none; border: none; cursor: pointer; color: var(--dim);
|
|
701
|
+
font-size: 13px; padding: 0 2px; line-height: 1; flex-shrink: 0;
|
|
702
|
+
border-radius: 3px; margin-left: 2px; opacity: 0;
|
|
703
|
+
}
|
|
704
|
+
.agent-tab:hover .agent-tab-close { opacity: 1; }
|
|
705
|
+
.agent-tab-close:hover { background: var(--border); color: var(--text); }
|
|
706
|
+
.tab-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
|
707
|
+
.tab-dot-running { background: var(--yellow); animation: tab-pulse 1s ease-in-out infinite; }
|
|
708
|
+
.tab-dot-ok { background: var(--green); }
|
|
709
|
+
.tab-dot-error { background: var(--red); }
|
|
710
|
+
.tab-dot-info { background: var(--muted); }
|
|
711
|
+
@keyframes tab-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
712
|
+
#termBtn.term-error { color: var(--red); border-color: var(--red); }
|
|
678
713
|
.file-row-more-btn {
|
|
679
714
|
background: none; border: none; cursor: pointer; font-size: 14px; color: var(--dim);
|
|
680
715
|
padding: 0 4px; line-height: 1; opacity: 0; transition: opacity 0.1s;
|
|
@@ -783,6 +818,7 @@
|
|
|
783
818
|
<button class="agent-panel-cancel" id="agentCancelBtn" onclick="cancelAgent()" title="Сбросить состояние агента" style="display:none">⏹ сброс</button>
|
|
784
819
|
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
785
820
|
</div>
|
|
821
|
+
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
786
822
|
<div class="agent-terminal" id="agentTerminal"></div>
|
|
787
823
|
</div>
|
|
788
824
|
|
|
@@ -824,6 +860,155 @@ async function runCoverage() {
|
|
|
824
860
|
// ─── Agent ────────────────────────────────────────────────────────────────────
|
|
825
861
|
let agentRunning = false;
|
|
826
862
|
|
|
863
|
+
// ─── Console Sessions ─────────────────────────────────────────────────────────
|
|
864
|
+
const consoleSessions = []; // { id, title, lines, status, startTime }
|
|
865
|
+
let activeSessionId = null; // currently viewed tab
|
|
866
|
+
let runningSessionId = null; // tab that is currently receiving output
|
|
867
|
+
const SESSION_MAX = 25;
|
|
868
|
+
const SESSIONS_KEY = 'viberadar_sessions';
|
|
869
|
+
|
|
870
|
+
function _sessionId() {
|
|
871
|
+
return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function saveSessions() {
|
|
875
|
+
try {
|
|
876
|
+
const data = consoleSessions.slice(-SESSION_MAX).map(s => ({
|
|
877
|
+
...s, lines: s.lines.slice(-500)
|
|
878
|
+
}));
|
|
879
|
+
localStorage.setItem(SESSIONS_KEY, JSON.stringify(data));
|
|
880
|
+
} catch {}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function restoreSessions() {
|
|
884
|
+
try {
|
|
885
|
+
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
886
|
+
if (!raw) return;
|
|
887
|
+
const saved = JSON.parse(raw);
|
|
888
|
+
consoleSessions.push(...saved);
|
|
889
|
+
for (const s of consoleSessions) {
|
|
890
|
+
if (s.status === 'running') {
|
|
891
|
+
s.status = 'error';
|
|
892
|
+
s.lines.push({ text: '⚡ Прервано (перезагрузка страницы)', isError: true });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (consoleSessions.length > 0) {
|
|
896
|
+
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
897
|
+
renderTabs();
|
|
898
|
+
renderActiveSession();
|
|
899
|
+
}
|
|
900
|
+
} catch {}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function createSession(title, status = 'running') {
|
|
904
|
+
if (consoleSessions.length >= SESSION_MAX) consoleSessions.shift();
|
|
905
|
+
const s = { id: _sessionId(), title, lines: [], status, startTime: Date.now() };
|
|
906
|
+
consoleSessions.push(s);
|
|
907
|
+
activeSessionId = s.id;
|
|
908
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
909
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
910
|
+
renderTabs();
|
|
911
|
+
renderActiveSession();
|
|
912
|
+
saveSessions();
|
|
913
|
+
return s.id;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function switchSession(id) {
|
|
917
|
+
activeSessionId = id;
|
|
918
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
919
|
+
if (s) {
|
|
920
|
+
const statusText = s.status === 'running' ? 'работает…'
|
|
921
|
+
: s.status === 'ok' ? '✅ готово'
|
|
922
|
+
: s.status === 'error' ? '❌ ошибка'
|
|
923
|
+
: '';
|
|
924
|
+
document.getElementById('agentPanelTitle').textContent = s.title;
|
|
925
|
+
document.getElementById('agentPanelStatus').textContent = statusText;
|
|
926
|
+
}
|
|
927
|
+
renderTabs();
|
|
928
|
+
renderActiveSession();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function closeSession(id) {
|
|
932
|
+
const idx = consoleSessions.findIndex(s => s.id === id);
|
|
933
|
+
if (idx === -1) return;
|
|
934
|
+
consoleSessions.splice(idx, 1);
|
|
935
|
+
if (activeSessionId === id) {
|
|
936
|
+
activeSessionId = consoleSessions.length > 0
|
|
937
|
+
? consoleSessions[Math.min(idx, consoleSessions.length - 1)].id
|
|
938
|
+
: null;
|
|
939
|
+
}
|
|
940
|
+
renderTabs();
|
|
941
|
+
renderActiveSession();
|
|
942
|
+
saveSessions();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function appendToSession(id, lineOrNode, isError = false, isDim = false) {
|
|
946
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
947
|
+
if (!s) return;
|
|
948
|
+
let stored;
|
|
949
|
+
if (typeof lineOrNode === 'string') {
|
|
950
|
+
stored = { text: lineOrNode, isError, isDim };
|
|
951
|
+
} else {
|
|
952
|
+
stored = { html: lineOrNode.outerHTML };
|
|
953
|
+
}
|
|
954
|
+
s.lines.push(stored);
|
|
955
|
+
if (activeSessionId === id) {
|
|
956
|
+
const term = document.getElementById('agentTerminal');
|
|
957
|
+
const el = document.createElement('div');
|
|
958
|
+
if (stored.html) {
|
|
959
|
+
el.innerHTML = stored.html;
|
|
960
|
+
} else {
|
|
961
|
+
el.className = 'agent-line' + (isError ? ' err' : isDim ? ' dim' : '');
|
|
962
|
+
el.textContent = lineOrNode;
|
|
963
|
+
}
|
|
964
|
+
term.appendChild(el);
|
|
965
|
+
term.scrollTop = term.scrollHeight;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function updateSessionStatus(id, status) {
|
|
970
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
971
|
+
if (!s) return;
|
|
972
|
+
s.status = status;
|
|
973
|
+
renderTabs();
|
|
974
|
+
saveSessions();
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function renderTabs() {
|
|
978
|
+
const bar = document.getElementById('agentTabsBar');
|
|
979
|
+
if (!bar) return;
|
|
980
|
+
bar.innerHTML = consoleSessions.map(s => {
|
|
981
|
+
const isActive = s.id === activeSessionId;
|
|
982
|
+
const safe = s.title.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
983
|
+
return `<div class="agent-tab${isActive ? ' active' : ''}" onclick="switchSession('${s.id}')">
|
|
984
|
+
<span class="tab-dot tab-dot-${s.status}"></span>
|
|
985
|
+
<span class="agent-tab-title" title="${safe}">${safe}</span>
|
|
986
|
+
<button class="agent-tab-close" onclick="event.stopPropagation();closeSession('${s.id}')" title="Закрыть вкладку">×</button>
|
|
987
|
+
</div>`;
|
|
988
|
+
}).join('');
|
|
989
|
+
const active = bar.querySelector('.agent-tab.active');
|
|
990
|
+
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function renderActiveSession() {
|
|
994
|
+
const term = document.getElementById('agentTerminal');
|
|
995
|
+
if (!term) return;
|
|
996
|
+
term.innerHTML = '';
|
|
997
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
998
|
+
if (!s) return;
|
|
999
|
+
for (const ln of s.lines) {
|
|
1000
|
+
const el = document.createElement('div');
|
|
1001
|
+
if (ln.html) {
|
|
1002
|
+
el.innerHTML = ln.html;
|
|
1003
|
+
} else {
|
|
1004
|
+
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1005
|
+
el.textContent = ln.text;
|
|
1006
|
+
}
|
|
1007
|
+
term.appendChild(el);
|
|
1008
|
+
}
|
|
1009
|
+
term.scrollTop = term.scrollHeight;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
827
1012
|
async function setAgent(agent) {
|
|
828
1013
|
await fetch('/api/set-agent', {
|
|
829
1014
|
method: 'POST',
|
|
@@ -858,7 +1043,13 @@ async function cancelAgent() {
|
|
|
858
1043
|
setAgentRunning(false);
|
|
859
1044
|
updateQueueBadge(0);
|
|
860
1045
|
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
861
|
-
|
|
1046
|
+
if (runningSessionId) {
|
|
1047
|
+
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1048
|
+
updateSessionStatus(runningSessionId, 'error');
|
|
1049
|
+
runningSessionId = null;
|
|
1050
|
+
} else {
|
|
1051
|
+
appendTerminalLine('⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1052
|
+
}
|
|
862
1053
|
}
|
|
863
1054
|
|
|
864
1055
|
async function clearAgentQueue() {
|
|
@@ -981,7 +1172,6 @@ async function runAgentTask(task, featureKey, filePath) {
|
|
|
981
1172
|
document.getElementById('agentPanel').classList.add('open');
|
|
982
1173
|
document.getElementById('termBtn').classList.add('term-active');
|
|
983
1174
|
if (!agentRunning) {
|
|
984
|
-
document.getElementById('agentTerminal').innerHTML = '';
|
|
985
1175
|
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
986
1176
|
}
|
|
987
1177
|
await fetch('/api/run-agent', {
|
|
@@ -992,11 +1182,9 @@ async function runAgentTask(task, featureKey, filePath) {
|
|
|
992
1182
|
}
|
|
993
1183
|
|
|
994
1184
|
async function runTests(featureKey, testType) {
|
|
995
|
-
document.getElementById('agentTerminal').innerHTML = '';
|
|
996
|
-
document.getElementById('agentPanelTitle').textContent = `🧪 ${testType} тесты`;
|
|
997
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
998
1185
|
document.getElementById('agentPanel').classList.add('open');
|
|
999
1186
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1187
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1000
1188
|
await fetch('/api/run-tests', {
|
|
1001
1189
|
method: 'POST',
|
|
1002
1190
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1017,12 +1205,11 @@ function toggleAgentPanel() {
|
|
|
1017
1205
|
}
|
|
1018
1206
|
|
|
1019
1207
|
function appendTerminalLine(line, isError, isDim) {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
term.scrollTop = term.scrollHeight;
|
|
1208
|
+
if (!activeSessionId) {
|
|
1209
|
+
const id = createSession('Консоль', 'info');
|
|
1210
|
+
activeSessionId = id;
|
|
1211
|
+
}
|
|
1212
|
+
appendToSession(activeSessionId, line, !!isError, !!isDim);
|
|
1026
1213
|
}
|
|
1027
1214
|
|
|
1028
1215
|
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
@@ -1989,36 +2176,50 @@ function connectSSE() {
|
|
|
1989
2176
|
updateQueueBadge(queueLength);
|
|
1990
2177
|
document.getElementById('agentPanel').classList.add('open');
|
|
1991
2178
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1992
|
-
|
|
2179
|
+
// Append queue notification to the currently running session (or active)
|
|
2180
|
+
const targetId = runningSessionId || activeSessionId;
|
|
2181
|
+
if (targetId) {
|
|
2182
|
+
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
2183
|
+
}
|
|
1993
2184
|
});
|
|
1994
2185
|
|
|
1995
2186
|
es.addEventListener('agent-started', (e) => {
|
|
1996
2187
|
setAgentRunning(true);
|
|
1997
2188
|
const { title, queueLength = 0 } = JSON.parse(e.data);
|
|
1998
2189
|
updateQueueBadge(queueLength);
|
|
2190
|
+
const id = createSession(title);
|
|
2191
|
+
runningSessionId = id;
|
|
1999
2192
|
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
2000
2193
|
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
2001
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
2002
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
2003
|
-
document.getElementById('agentTerminal').innerHTML = '';
|
|
2004
2194
|
});
|
|
2005
2195
|
|
|
2006
2196
|
es.addEventListener('agent-output', (e) => {
|
|
2007
2197
|
const { line, isError, isDim } = JSON.parse(e.data);
|
|
2008
|
-
|
|
2009
|
-
|
|
2198
|
+
if (runningSessionId) {
|
|
2199
|
+
appendToSession(runningSessionId, line, !!isError, !!isDim);
|
|
2200
|
+
if (activeSessionId === runningSessionId) {
|
|
2201
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
2202
|
+
}
|
|
2203
|
+
} else {
|
|
2204
|
+
appendTerminalLine(line, !!isError, !!isDim);
|
|
2205
|
+
}
|
|
2010
2206
|
});
|
|
2011
2207
|
|
|
2012
2208
|
es.addEventListener('agent-done', () => {
|
|
2013
2209
|
setAgentRunning(false);
|
|
2014
2210
|
updateQueueBadge(0);
|
|
2015
|
-
|
|
2211
|
+
if (runningSessionId) {
|
|
2212
|
+
updateSessionStatus(runningSessionId, 'ok');
|
|
2213
|
+
if (activeSessionId === runningSessionId) {
|
|
2214
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
2215
|
+
}
|
|
2216
|
+
runningSessionId = null;
|
|
2217
|
+
}
|
|
2016
2218
|
renderContent();
|
|
2017
2219
|
});
|
|
2018
2220
|
|
|
2019
2221
|
es.addEventListener('agent-summary', (e) => {
|
|
2020
2222
|
const { passed, failed, files } = JSON.parse(e.data);
|
|
2021
|
-
const term = document.getElementById('agentTerminal');
|
|
2022
2223
|
const allOk = failed === 0;
|
|
2023
2224
|
const box = document.createElement('div');
|
|
2024
2225
|
box.style.cssText = `
|
|
@@ -2035,28 +2236,46 @@ function connectSSE() {
|
|
|
2035
2236
|
</div>
|
|
2036
2237
|
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
2037
2238
|
`;
|
|
2038
|
-
|
|
2039
|
-
|
|
2239
|
+
const targetId = runningSessionId || activeSessionId;
|
|
2240
|
+
if (targetId) appendToSession(targetId, box);
|
|
2040
2241
|
});
|
|
2041
2242
|
|
|
2042
2243
|
es.addEventListener('agent-error', (e) => {
|
|
2043
2244
|
setAgentRunning(false);
|
|
2044
2245
|
const { message } = JSON.parse(e.data);
|
|
2045
|
-
|
|
2046
|
-
|
|
2246
|
+
if (runningSessionId) {
|
|
2247
|
+
appendToSession(runningSessionId, '❌ ' + (message || 'Ошибка агента'), true);
|
|
2248
|
+
updateSessionStatus(runningSessionId, 'error');
|
|
2249
|
+
if (activeSessionId === runningSessionId) {
|
|
2250
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
2251
|
+
}
|
|
2252
|
+
runningSessionId = null;
|
|
2253
|
+
} else {
|
|
2254
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
2255
|
+
appendTerminalLine('❌ ' + (message || 'Ошибка агента'), true);
|
|
2256
|
+
}
|
|
2047
2257
|
});
|
|
2048
2258
|
|
|
2049
2259
|
es.addEventListener('tests-started', (e) => {
|
|
2050
2260
|
const { testType, count } = JSON.parse(e.data);
|
|
2261
|
+
const id = createSession(`🧪 ${testType}`);
|
|
2262
|
+
runningSessionId = id;
|
|
2263
|
+
appendToSession(id, `Запускаю ${testType} тесты (${count} файлов)…`);
|
|
2051
2264
|
document.getElementById('agentPanelTitle').textContent = `🧪 ${testType} тесты`;
|
|
2052
2265
|
document.getElementById('agentPanelStatus').textContent = `запускаю ${count} файлов…`;
|
|
2053
2266
|
});
|
|
2054
2267
|
|
|
2055
2268
|
es.addEventListener('tests-done', (e) => {
|
|
2056
2269
|
const { passed, failed, testErrors } = JSON.parse(e.data);
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2270
|
+
const status = failed === 0 ? 'ok' : 'error';
|
|
2271
|
+
if (runningSessionId) {
|
|
2272
|
+
updateSessionStatus(runningSessionId, status);
|
|
2273
|
+
if (activeSessionId === runningSessionId) {
|
|
2274
|
+
document.getElementById('agentPanelStatus').textContent =
|
|
2275
|
+
failed === 0 ? `✅ ${passed} passed` : `⚠️ ${passed} passed, ${failed} failed`;
|
|
2276
|
+
}
|
|
2277
|
+
runningSessionId = null;
|
|
2278
|
+
}
|
|
2060
2279
|
D.testErrors = testErrors || {};
|
|
2061
2280
|
renderContent();
|
|
2062
2281
|
});
|
|
@@ -2068,7 +2287,7 @@ function connectSSE() {
|
|
|
2068
2287
|
};
|
|
2069
2288
|
}
|
|
2070
2289
|
|
|
2071
|
-
init().then(() => connectSSE());
|
|
2290
|
+
init().then(() => { restoreSessions(); connectSSE(); });
|
|
2072
2291
|
</script>
|
|
2073
2292
|
</body>
|
|
2074
2293
|
</html>
|