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.
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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
- appendTerminalLine('⏹ Состояние агента сброшено (очередь очищена)', false);
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
- const term = document.getElementById('agentTerminal');
1021
- const el = document.createElement('div');
1022
- el.className = 'agent-line' + (isError ? ' err' : isDim ? ' dim' : '');
1023
- el.textContent = line;
1024
- term.appendChild(el);
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
- appendTerminalLine(`📋 В очереди (${queueLength}): ${title}`, false);
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
- appendTerminalLine(line, !!isError, !!isDim);
2009
- document.getElementById('agentPanelStatus').textContent = 'работает…';
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
- document.getElementById('agentPanelStatus').textContent = '✅ готово';
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
- term.appendChild(box);
2039
- term.scrollTop = term.scrollHeight;
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
- document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
2046
- appendTerminalLine('❌ ' + (message || 'Ошибка агента'), true);
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
- document.getElementById('agentPanelStatus').textContent =
2058
- failed === 0 ? `✅ ${passed} passed` : `⚠️ ${passed} passed, ${failed} failed`;
2059
- // Apply testErrors directly from event — no separate fetch needed
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.37",
3
+ "version": "0.3.38",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {