viberadar 0.3.59 → 0.3.61
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/server/index.d.ts.map +1 -1
- package/dist/server/index.js +502 -108
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +1060 -411
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -611,17 +611,19 @@
|
|
|
611
611
|
.tt-fix-btn:hover { background: rgba(255,200,0,0.1); color: var(--yellow); border-color: var(--yellow); }
|
|
612
612
|
.tt-write-btn { border-color: var(--accent); color: var(--accent); }
|
|
613
613
|
.tt-write-btn:hover { background: rgba(88,166,255,0.1); color: var(--accent); border-color: var(--accent); }
|
|
614
|
-
.file-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
615
|
-
.file-row {
|
|
616
|
-
display: flex;
|
|
617
|
-
align-items: center;
|
|
618
|
-
gap: 8px;
|
|
619
|
-
padding: 7px 10px;
|
|
614
|
+
.file-rows { display: flex; flex-direction: column; gap: 2px; contain: content; }
|
|
615
|
+
.file-row {
|
|
616
|
+
display: flex;
|
|
617
|
+
align-items: center;
|
|
618
|
+
gap: 8px;
|
|
619
|
+
padding: 7px 10px;
|
|
620
620
|
border-radius: 6px;
|
|
621
621
|
cursor: pointer;
|
|
622
|
-
font-size: 13px;
|
|
623
|
-
transition: background 0.1s;
|
|
624
|
-
|
|
622
|
+
font-size: 13px;
|
|
623
|
+
transition: background 0.1s;
|
|
624
|
+
content-visibility: auto;
|
|
625
|
+
contain-intrinsic-size: 34px;
|
|
626
|
+
}
|
|
625
627
|
.file-row:hover { background: var(--bg-card); }
|
|
626
628
|
.file-row.active { background: var(--bg-hover); border-left: 2px solid var(--blue); padding-left: 8px; }
|
|
627
629
|
.file-row-icon { font-size: 12px; flex-shrink: 0; }
|
|
@@ -673,10 +675,12 @@
|
|
|
673
675
|
.file-agent-spinner.running { border: 2px solid var(--yellow); border-top-color: transparent; animation: spin 0.7s linear infinite; }
|
|
674
676
|
.file-agent-spinner.queued { border: 2px solid var(--dim); border-top-color: transparent; animation: spin 1.5s linear infinite; }
|
|
675
677
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
676
|
-
.file-row-errors {
|
|
677
|
-
padding: 4px 10px 6px 32px;
|
|
678
|
-
display: flex; flex-direction: column; gap: 3px;
|
|
679
|
-
|
|
678
|
+
.file-row-errors {
|
|
679
|
+
padding: 4px 10px 6px 32px;
|
|
680
|
+
display: flex; flex-direction: column; gap: 3px;
|
|
681
|
+
content-visibility: auto;
|
|
682
|
+
contain-intrinsic-size: 46px;
|
|
683
|
+
}
|
|
680
684
|
.err-item { display: flex; flex-direction: column; gap: 1px; }
|
|
681
685
|
.err-name { font-size: 11px; color: var(--muted); }
|
|
682
686
|
.err-msg { font-size: 10px; color: var(--red); font-family: monospace; opacity: 0.85; }
|
|
@@ -765,10 +769,111 @@
|
|
|
765
769
|
cursor: pointer; font-size: 11px; padding: 2px 8px; border-radius: 4px;
|
|
766
770
|
}
|
|
767
771
|
.agent-panel-cancel:hover { background: var(--yellow); color: #000; }
|
|
768
|
-
.agent-queue-badge {
|
|
769
|
-
font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
|
|
770
|
-
border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
|
|
771
|
-
}
|
|
772
|
+
.agent-queue-badge {
|
|
773
|
+
font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
|
|
774
|
+
border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
|
|
775
|
+
}
|
|
776
|
+
.agent-toolbar {
|
|
777
|
+
display: flex;
|
|
778
|
+
align-items: center;
|
|
779
|
+
gap: 8px;
|
|
780
|
+
padding: 6px 12px;
|
|
781
|
+
background: #0b1018;
|
|
782
|
+
border-bottom: 1px solid var(--border);
|
|
783
|
+
flex-wrap: wrap;
|
|
784
|
+
}
|
|
785
|
+
.agent-toolbar input[type="text"] {
|
|
786
|
+
min-width: 220px;
|
|
787
|
+
padding: 4px 8px;
|
|
788
|
+
border-radius: 4px;
|
|
789
|
+
border: 1px solid var(--border);
|
|
790
|
+
background: #0b1220;
|
|
791
|
+
color: var(--text);
|
|
792
|
+
font-size: 11px;
|
|
793
|
+
}
|
|
794
|
+
.agent-toolbar label {
|
|
795
|
+
display: inline-flex;
|
|
796
|
+
align-items: center;
|
|
797
|
+
gap: 5px;
|
|
798
|
+
font-size: 11px;
|
|
799
|
+
color: var(--muted);
|
|
800
|
+
user-select: none;
|
|
801
|
+
}
|
|
802
|
+
.agent-toolbar-btn {
|
|
803
|
+
background: none;
|
|
804
|
+
border: 1px solid var(--border);
|
|
805
|
+
color: var(--muted);
|
|
806
|
+
cursor: pointer;
|
|
807
|
+
font-size: 11px;
|
|
808
|
+
padding: 3px 8px;
|
|
809
|
+
border-radius: 4px;
|
|
810
|
+
}
|
|
811
|
+
.agent-toolbar-btn:hover { color: var(--text); border-color: var(--blue); }
|
|
812
|
+
.agent-toolbar-meta { font-size: 11px; color: var(--dim); margin-left: auto; }
|
|
813
|
+
.agent-queue-panel {
|
|
814
|
+
display: none;
|
|
815
|
+
border-bottom: 1px solid var(--border);
|
|
816
|
+
background: #090f18;
|
|
817
|
+
padding: 8px 12px;
|
|
818
|
+
max-height: 110px;
|
|
819
|
+
overflow-y: auto;
|
|
820
|
+
}
|
|
821
|
+
.agent-queue-title {
|
|
822
|
+
font-size: 11px;
|
|
823
|
+
color: var(--muted);
|
|
824
|
+
margin-bottom: 6px;
|
|
825
|
+
font-weight: 600;
|
|
826
|
+
}
|
|
827
|
+
.agent-queue-item {
|
|
828
|
+
display: flex;
|
|
829
|
+
align-items: center;
|
|
830
|
+
gap: 8px;
|
|
831
|
+
font-size: 11px;
|
|
832
|
+
color: var(--text);
|
|
833
|
+
margin-bottom: 4px;
|
|
834
|
+
}
|
|
835
|
+
.agent-queue-item:last-child { margin-bottom: 0; }
|
|
836
|
+
.agent-queue-pos {
|
|
837
|
+
color: var(--dim);
|
|
838
|
+
min-width: 24px;
|
|
839
|
+
}
|
|
840
|
+
.agent-queue-actions { margin-left: auto; display: inline-flex; gap: 4px; }
|
|
841
|
+
.agent-queue-action {
|
|
842
|
+
border: 1px solid var(--border);
|
|
843
|
+
background: transparent;
|
|
844
|
+
color: var(--muted);
|
|
845
|
+
border-radius: 4px;
|
|
846
|
+
padding: 1px 6px;
|
|
847
|
+
cursor: pointer;
|
|
848
|
+
font-size: 10px;
|
|
849
|
+
line-height: 1.4;
|
|
850
|
+
}
|
|
851
|
+
.agent-queue-action:hover { border-color: var(--blue); color: var(--text); }
|
|
852
|
+
.agent-summary-matrix {
|
|
853
|
+
display: none;
|
|
854
|
+
border-bottom: 1px solid var(--border);
|
|
855
|
+
background: #090f17;
|
|
856
|
+
padding: 8px 12px;
|
|
857
|
+
max-height: 150px;
|
|
858
|
+
overflow: auto;
|
|
859
|
+
font-size: 11px;
|
|
860
|
+
}
|
|
861
|
+
.agent-summary-title {
|
|
862
|
+
color: var(--muted);
|
|
863
|
+
font-weight: 600;
|
|
864
|
+
margin-bottom: 6px;
|
|
865
|
+
}
|
|
866
|
+
.agent-summary-row {
|
|
867
|
+
display: grid;
|
|
868
|
+
grid-template-columns: 60px 1fr auto;
|
|
869
|
+
gap: 8px;
|
|
870
|
+
margin-bottom: 4px;
|
|
871
|
+
align-items: center;
|
|
872
|
+
}
|
|
873
|
+
.agent-summary-status-covered { color: var(--green); }
|
|
874
|
+
.agent-summary-status-not-covered { color: var(--red); }
|
|
875
|
+
.agent-summary-status-blocked { color: var(--yellow); }
|
|
876
|
+
.agent-summary-status-infra { color: var(--dim); }
|
|
772
877
|
/* ── Console Tabs ───────────────────────────────────────────────────────── */
|
|
773
878
|
.agent-tabs-bar {
|
|
774
879
|
display: flex; align-items: stretch; overflow-x: auto;
|
|
@@ -885,11 +990,13 @@
|
|
|
885
990
|
font-size: 12px;
|
|
886
991
|
line-height: 1.5;
|
|
887
992
|
}
|
|
888
|
-
.agent-line { color: #c9d1d9; }
|
|
889
|
-
.agent-line.err { color: var(--red); }
|
|
890
|
-
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
891
|
-
|
|
892
|
-
|
|
993
|
+
.agent-line { color: #c9d1d9; }
|
|
994
|
+
.agent-line.err { color: var(--red); }
|
|
995
|
+
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
996
|
+
.agent-line.match { background: rgba(31, 111, 235, 0.22); }
|
|
997
|
+
.agent-line.command { color: #79c0ff; }
|
|
998
|
+
|
|
999
|
+
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
893
1000
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
894
1001
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
895
1002
|
/* ─── E2E Plan UI ─────────────────────────────────────────────────── */
|
|
@@ -1001,19 +1108,35 @@
|
|
|
1001
1108
|
<div id="panelContent"></div>
|
|
1002
1109
|
</div>
|
|
1003
1110
|
|
|
1004
|
-
<div class="agent-panel" id="agentPanel">
|
|
1005
|
-
<div class="agent-panel-header">
|
|
1006
|
-
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
1007
|
-
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
1111
|
+
<div class="agent-panel" id="agentPanel">
|
|
1112
|
+
<div class="agent-panel-header">
|
|
1113
|
+
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
1114
|
+
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
1008
1115
|
<span class="agent-queue-badge" id="agentQueueBadge" style="display:none">📋 <span id="agentQueueCount">0</span> в очереди</span>
|
|
1009
1116
|
<button class="agent-panel-cancel" id="agentQueueClearBtn" onclick="clearAgentQueue()" title="Очистить очередь" style="display:none">🗑 очередь</button>
|
|
1010
1117
|
<button class="agent-panel-cancel" id="agentCancelBtn" onclick="cancelAgent()" title="Сбросить состояние агента" style="display:none">⏹ сброс</button>
|
|
1011
|
-
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
1012
|
-
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
1013
|
-
</div>
|
|
1014
|
-
<div class="agent-
|
|
1015
|
-
|
|
1016
|
-
</
|
|
1118
|
+
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
1119
|
+
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
1120
|
+
</div>
|
|
1121
|
+
<div class="agent-toolbar">
|
|
1122
|
+
<input id="agentSearchInput" type="text" placeholder="Поиск по терминалу..." />
|
|
1123
|
+
<label><input id="agentSearchErrorsOnly" type="checkbox" /> only errors</label>
|
|
1124
|
+
<label><input id="agentSearchCurrentRunOnly" type="checkbox" /> current run</label>
|
|
1125
|
+
<label><input id="agentSearchRegex" type="checkbox" /> regex</label>
|
|
1126
|
+
<button class="agent-toolbar-btn" onclick="jumpTerminalMatch(-1)">↑ match</button>
|
|
1127
|
+
<button class="agent-toolbar-btn" onclick="jumpTerminalMatch(1)">↓ match</button>
|
|
1128
|
+
<button class="agent-toolbar-btn" onclick="jumpCommand(-1)">↑ command</button>
|
|
1129
|
+
<button class="agent-toolbar-btn" onclick="jumpCommand(1)">↓ command</button>
|
|
1130
|
+
<button class="agent-toolbar-btn" onclick="jumpError(-1)">↑ error</button>
|
|
1131
|
+
<button class="agent-toolbar-btn" onclick="jumpError(1)">↓ error</button>
|
|
1132
|
+
<button class="agent-toolbar-btn" onclick="exportActiveRun()">Export run</button>
|
|
1133
|
+
<span class="agent-toolbar-meta" id="agentSearchMeta">0 matches</span>
|
|
1134
|
+
</div>
|
|
1135
|
+
<div class="agent-queue-panel" id="agentQueuePanel"></div>
|
|
1136
|
+
<div class="agent-summary-matrix" id="agentSummaryMatrix"></div>
|
|
1137
|
+
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
1138
|
+
<div class="agent-terminal" id="agentTerminal"></div>
|
|
1139
|
+
</div>
|
|
1017
1140
|
|
|
1018
1141
|
<script>
|
|
1019
1142
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
@@ -1027,6 +1150,10 @@ let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string
|
|
|
1027
1150
|
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
1028
1151
|
let showOnlyUntestedInFeature = false; // source tab in feature detail
|
|
1029
1152
|
const selectedSourceFiles = new Set(); // normalized relative paths for batch actions
|
|
1153
|
+
const FILE_ROWS_INITIAL_LIMIT = 250;
|
|
1154
|
+
const FILE_ROWS_LIMIT_STEP = 250;
|
|
1155
|
+
let fileRowsRenderKey = '';
|
|
1156
|
+
let fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1030
1157
|
let e2ePlan = null; // current E2E plan object
|
|
1031
1158
|
let e2ePlanLoading = false;
|
|
1032
1159
|
const modeStore = {
|
|
@@ -1091,8 +1218,11 @@ function switchMode(nextMode) {
|
|
|
1091
1218
|
renderSidebar();
|
|
1092
1219
|
renderContent();
|
|
1093
1220
|
}
|
|
1094
|
-
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1095
|
-
let runAllRunning = false;
|
|
1221
|
+
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1222
|
+
let runAllRunning = false;
|
|
1223
|
+
let refreshDataInFlight = false;
|
|
1224
|
+
let refreshDataQueued = false;
|
|
1225
|
+
let refreshDataTimer = null;
|
|
1096
1226
|
|
|
1097
1227
|
function escapeHtml(text) {
|
|
1098
1228
|
return String(text || '')
|
|
@@ -1131,16 +1261,42 @@ function setFeatureDrill(featureKey, syncHash = true) {
|
|
|
1131
1261
|
renderContent();
|
|
1132
1262
|
}
|
|
1133
1263
|
|
|
1134
|
-
function selectedFilesForFeature(featureKey, visibleSourceFiles = null) {
|
|
1135
|
-
const visibleSet = visibleSourceFiles ? new Set(visibleSourceFiles.map(p => p.replace(/\\/g, '/'))) : null;
|
|
1136
|
-
return Array.from(selectedSourceFiles).filter((relPath) => {
|
|
1264
|
+
function selectedFilesForFeature(featureKey, visibleSourceFiles = null) {
|
|
1265
|
+
const visibleSet = visibleSourceFiles ? new Set(visibleSourceFiles.map(p => p.replace(/\\/g, '/'))) : null;
|
|
1266
|
+
return Array.from(selectedSourceFiles).filter((relPath) => {
|
|
1137
1267
|
if (visibleSet && !visibleSet.has(relPath)) return false;
|
|
1138
1268
|
const mod = D?.modules?.find((m) => m.relativePath.replace(/\\/g, '/') === relPath);
|
|
1139
|
-
return !!mod && mod.type !== 'test' && mod.featureKeys?.includes(featureKey);
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
function
|
|
1269
|
+
return !!mod && mod.type !== 'test' && mod.featureKeys?.includes(featureKey);
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function getFileRowsWindow(items, renderKey) {
|
|
1274
|
+
if (fileRowsRenderKey !== renderKey) {
|
|
1275
|
+
fileRowsRenderKey = renderKey;
|
|
1276
|
+
fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1277
|
+
}
|
|
1278
|
+
const visibleRows = items.slice(0, fileRowsRenderLimit);
|
|
1279
|
+
const hiddenRows = Math.max(0, items.length - visibleRows.length);
|
|
1280
|
+
return { visibleRows, hiddenRows, hasMoreRows: hiddenRows > 0 };
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function increaseFileRowsLimit() {
|
|
1284
|
+
fileRowsRenderLimit += FILE_ROWS_LIMIT_STEP;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function bindFileRowsClick(container) {
|
|
1288
|
+
const fileRows = container.querySelector('#fileRows');
|
|
1289
|
+
if (!fileRows) return;
|
|
1290
|
+
fileRows.onclick = (event) => {
|
|
1291
|
+
const row = event.target.closest('.file-row[data-id]');
|
|
1292
|
+
if (!row) return;
|
|
1293
|
+
const moduleId = row.dataset.id;
|
|
1294
|
+
const mod = D.modules.find((m) => String(m.id) === moduleId);
|
|
1295
|
+
if (mod) openModulePanel(mod);
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function applyHashRoute() {
|
|
1144
1300
|
const hash = (window.location.hash || '').replace(/^#/, '');
|
|
1145
1301
|
if (!hash) {
|
|
1146
1302
|
if (view === 'features' && drillFeatureKey) {
|
|
@@ -1188,70 +1344,102 @@ async function runAllTests() {
|
|
|
1188
1344
|
}
|
|
1189
1345
|
|
|
1190
1346
|
// ─── Agent ────────────────────────────────────────────────────────────────────
|
|
1191
|
-
let agentRunning = false;
|
|
1192
|
-
const agentRunningPaths = new Set(); // paths of files currently being processed by agent
|
|
1193
|
-
const agentQueuedPaths = new Set(); // paths of files waiting in queue
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
let
|
|
1198
|
-
let
|
|
1199
|
-
|
|
1200
|
-
|
|
1347
|
+
let agentRunning = false;
|
|
1348
|
+
const agentRunningPaths = new Set(); // paths of files currently being processed by agent
|
|
1349
|
+
const agentQueuedPaths = new Set(); // paths of files waiting in queue
|
|
1350
|
+
let agentQueueState = [];
|
|
1351
|
+
let agentRunsState = [];
|
|
1352
|
+
let agentActiveRun = null;
|
|
1353
|
+
let currentRunId = null;
|
|
1354
|
+
let lastRunSummary = null;
|
|
1355
|
+
|
|
1356
|
+
// ─── Console Sessions ─────────────────────────────────────────────────────────
|
|
1357
|
+
const consoleSessions = []; // { id, title, lines, status, startTime }
|
|
1358
|
+
let activeSessionId = null; // currently viewed tab
|
|
1359
|
+
let runningSessionId = null; // tab that is currently receiving output
|
|
1360
|
+
const runSessionMap = new Map(); // runId -> sessionId
|
|
1361
|
+
const sessionRunMap = new Map(); // sessionId -> runId
|
|
1362
|
+
const SESSION_MAX = 25;
|
|
1363
|
+
const SESSION_LINE_LIMIT = 3000;
|
|
1364
|
+
const SESSIONS_KEY = 'viberadar_sessions';
|
|
1365
|
+
let terminalSearchQuery = '';
|
|
1366
|
+
let terminalSearchErrorsOnly = false;
|
|
1367
|
+
let terminalSearchCurrentRunOnly = false;
|
|
1368
|
+
let terminalSearchRegex = false;
|
|
1369
|
+
let terminalMatchRefs = [];
|
|
1370
|
+
let terminalCommandRefs = [];
|
|
1371
|
+
let terminalErrorRefs = [];
|
|
1372
|
+
let terminalMatchCursor = -1;
|
|
1373
|
+
let terminalCommandCursor = -1;
|
|
1374
|
+
let terminalErrorCursor = -1;
|
|
1201
1375
|
|
|
1202
1376
|
function _sessionId() {
|
|
1203
1377
|
return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
1204
1378
|
}
|
|
1205
1379
|
|
|
1206
|
-
function saveSessions() {
|
|
1207
|
-
try {
|
|
1208
|
-
const data = consoleSessions.slice(-SESSION_MAX).map(s => ({
|
|
1209
|
-
...s,
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1380
|
+
function saveSessions() {
|
|
1381
|
+
try {
|
|
1382
|
+
const data = consoleSessions.slice(-SESSION_MAX).map(s => ({
|
|
1383
|
+
...s,
|
|
1384
|
+
runId: sessionRunMap.get(s.id) || s.runId || null,
|
|
1385
|
+
lines: s.lines.slice(-500)
|
|
1386
|
+
}));
|
|
1387
|
+
localStorage.setItem(SESSIONS_KEY, JSON.stringify(data));
|
|
1388
|
+
} catch {}
|
|
1389
|
+
}
|
|
1214
1390
|
|
|
1215
1391
|
function restoreSessions() {
|
|
1216
1392
|
try {
|
|
1217
|
-
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
1218
|
-
if (!raw) return;
|
|
1219
|
-
const saved = JSON.parse(raw);
|
|
1220
|
-
consoleSessions.push(...saved);
|
|
1221
|
-
for (const s of consoleSessions) {
|
|
1222
|
-
if (s.
|
|
1223
|
-
s.
|
|
1224
|
-
s.
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
if (consoleSessions.length > 0) {
|
|
1228
|
-
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
1229
|
-
renderTabs();
|
|
1230
|
-
renderActiveSession();
|
|
1393
|
+
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
1394
|
+
if (!raw) return;
|
|
1395
|
+
const saved = JSON.parse(raw);
|
|
1396
|
+
consoleSessions.push(...saved);
|
|
1397
|
+
for (const s of consoleSessions) {
|
|
1398
|
+
if (s.runId) {
|
|
1399
|
+
runSessionMap.set(s.runId, s.id);
|
|
1400
|
+
sessionRunMap.set(s.id, s.runId);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (consoleSessions.length > 0) {
|
|
1404
|
+
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
1405
|
+
renderTabs();
|
|
1406
|
+
renderActiveSession();
|
|
1231
1407
|
}
|
|
1232
1408
|
} catch {}
|
|
1233
1409
|
}
|
|
1234
1410
|
|
|
1235
|
-
function createSession(title, status = 'running') {
|
|
1236
|
-
if (consoleSessions.length >= SESSION_MAX)
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1411
|
+
function createSession(title, status = 'running', runId = null) {
|
|
1412
|
+
if (consoleSessions.length >= SESSION_MAX) {
|
|
1413
|
+
const dropped = consoleSessions.shift();
|
|
1414
|
+
if (dropped?.id) {
|
|
1415
|
+
const droppedRunId = sessionRunMap.get(dropped.id);
|
|
1416
|
+
if (droppedRunId) runSessionMap.delete(droppedRunId);
|
|
1417
|
+
sessionRunMap.delete(dropped.id);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
const s = { id: _sessionId(), title, lines: [], status, startTime: Date.now(), runId };
|
|
1421
|
+
consoleSessions.push(s);
|
|
1422
|
+
activeSessionId = s.id;
|
|
1423
|
+
if (runId) {
|
|
1424
|
+
runSessionMap.set(runId, s.id);
|
|
1425
|
+
sessionRunMap.set(s.id, runId);
|
|
1426
|
+
}
|
|
1427
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
1428
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
1429
|
+
renderTabs();
|
|
1430
|
+
renderActiveSession();
|
|
1431
|
+
saveSessions();
|
|
1245
1432
|
return s.id;
|
|
1246
1433
|
}
|
|
1247
1434
|
|
|
1248
|
-
function switchSession(id) {
|
|
1249
|
-
activeSessionId = id;
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
: s.status === '
|
|
1435
|
+
function switchSession(id) {
|
|
1436
|
+
activeSessionId = id;
|
|
1437
|
+
currentRunId = sessionRunMap.get(id) || null;
|
|
1438
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
1439
|
+
if (s) {
|
|
1440
|
+
const statusText = s.status === 'running' ? 'работает…'
|
|
1441
|
+
: s.status === 'ok' ? '✅ готово'
|
|
1442
|
+
: s.status === 'error' ? '❌ ошибка'
|
|
1255
1443
|
: '';
|
|
1256
1444
|
document.getElementById('agentPanelTitle').textContent = s.title;
|
|
1257
1445
|
document.getElementById('agentPanelStatus').textContent = statusText;
|
|
@@ -1260,43 +1448,42 @@ function switchSession(id) {
|
|
|
1260
1448
|
renderActiveSession();
|
|
1261
1449
|
}
|
|
1262
1450
|
|
|
1263
|
-
function closeSession(id) {
|
|
1264
|
-
const idx = consoleSessions.findIndex(s => s.id === id);
|
|
1265
|
-
if (idx === -1) return;
|
|
1266
|
-
|
|
1267
|
-
if (
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1451
|
+
function closeSession(id) {
|
|
1452
|
+
const idx = consoleSessions.findIndex(s => s.id === id);
|
|
1453
|
+
if (idx === -1) return;
|
|
1454
|
+
const runId = sessionRunMap.get(id);
|
|
1455
|
+
if (runId) {
|
|
1456
|
+
sessionRunMap.delete(id);
|
|
1457
|
+
runSessionMap.delete(runId);
|
|
1458
|
+
}
|
|
1459
|
+
consoleSessions.splice(idx, 1);
|
|
1460
|
+
if (activeSessionId === id) {
|
|
1461
|
+
activeSessionId = consoleSessions.length > 0
|
|
1462
|
+
? consoleSessions[Math.min(idx, consoleSessions.length - 1)].id
|
|
1463
|
+
: null;
|
|
1271
1464
|
}
|
|
1272
1465
|
renderTabs();
|
|
1273
1466
|
renderActiveSession();
|
|
1274
1467
|
saveSessions();
|
|
1275
1468
|
}
|
|
1276
1469
|
|
|
1277
|
-
function appendToSession(id, lineOrNode, isError = false, isDim = false) {
|
|
1278
|
-
const s = consoleSessions.find(s => s.id === id);
|
|
1279
|
-
if (!s) return;
|
|
1470
|
+
function appendToSession(id, lineOrNode, isError = false, isDim = false) {
|
|
1471
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
1472
|
+
if (!s) return;
|
|
1280
1473
|
let stored;
|
|
1281
1474
|
if (typeof lineOrNode === 'string') {
|
|
1282
1475
|
stored = { text: lineOrNode, isError, isDim };
|
|
1283
1476
|
} else {
|
|
1284
1477
|
stored = { html: lineOrNode.outerHTML };
|
|
1285
|
-
}
|
|
1286
|
-
s.lines.push(stored);
|
|
1287
|
-
if (
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
el.textContent = lineOrNode;
|
|
1295
|
-
}
|
|
1296
|
-
term.appendChild(el);
|
|
1297
|
-
term.scrollTop = term.scrollHeight;
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1478
|
+
}
|
|
1479
|
+
s.lines.push(stored);
|
|
1480
|
+
if (s.lines.length > SESSION_LINE_LIMIT) {
|
|
1481
|
+
s.lines.splice(0, s.lines.length - SESSION_LINE_LIMIT);
|
|
1482
|
+
}
|
|
1483
|
+
if (activeSessionId === id) {
|
|
1484
|
+
renderActiveSession();
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1300
1487
|
|
|
1301
1488
|
function updateSessionStatus(id, status) {
|
|
1302
1489
|
const s = consoleSessions.find(s => s.id === id);
|
|
@@ -1324,9 +1511,9 @@ function renderTabs() {
|
|
|
1324
1511
|
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1325
1512
|
}
|
|
1326
1513
|
|
|
1327
|
-
function copyTerminalContent() {
|
|
1328
|
-
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1329
|
-
if (!s || s.lines.length === 0) return;
|
|
1514
|
+
function copyTerminalContent() {
|
|
1515
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1516
|
+
if (!s || s.lines.length === 0) return;
|
|
1330
1517
|
// Extract plain text from each line (strip HTML for rich nodes)
|
|
1331
1518
|
const text = s.lines.map(l => {
|
|
1332
1519
|
if (l.text !== undefined) return l.text;
|
|
@@ -1344,27 +1531,175 @@ function copyTerminalContent() {
|
|
|
1344
1531
|
btn.textContent = '✓';
|
|
1345
1532
|
btn.classList.add('copied');
|
|
1346
1533
|
setTimeout(() => { btn.textContent = prev; btn.classList.remove('copied'); }, 1500);
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function downloadTextArtifact(filename, content) {
|
|
1538
|
+
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
|
1539
|
+
const a = document.createElement('a');
|
|
1540
|
+
a.href = URL.createObjectURL(blob);
|
|
1541
|
+
a.download = filename;
|
|
1542
|
+
document.body.appendChild(a);
|
|
1543
|
+
a.click();
|
|
1544
|
+
setTimeout(() => {
|
|
1545
|
+
URL.revokeObjectURL(a.href);
|
|
1546
|
+
a.remove();
|
|
1547
|
+
}, 100);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function getActiveRunForExport() {
|
|
1551
|
+
const runId = sessionRunMap.get(activeSessionId) || currentRunId;
|
|
1552
|
+
if (runId) {
|
|
1553
|
+
const fromState = (agentRunsState || []).find((r) => r.runId === runId);
|
|
1554
|
+
if (fromState) return fromState;
|
|
1555
|
+
}
|
|
1556
|
+
return lastRunSummary || null;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function exportActiveRun() {
|
|
1560
|
+
const run = getActiveRunForExport();
|
|
1561
|
+
const session = consoleSessions.find((s) => s.id === activeSessionId);
|
|
1562
|
+
if (!run && !session) return;
|
|
1563
|
+
const mode = window.prompt('Формат экспорта: md или json', 'md');
|
|
1564
|
+
const format = (mode || 'md').trim().toLowerCase() === 'json' ? 'json' : 'md';
|
|
1565
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1566
|
+
const runId = run?.runId || sessionRunMap.get(activeSessionId) || 'session';
|
|
1567
|
+
if (format === 'json') {
|
|
1568
|
+
const payload = {
|
|
1569
|
+
run,
|
|
1570
|
+
sessionTitle: session?.title || null,
|
|
1571
|
+
lines: (session?.lines || []).map((line) => extractLineText(line)),
|
|
1572
|
+
exportedAt: new Date().toISOString(),
|
|
1573
|
+
};
|
|
1574
|
+
downloadTextArtifact(`viberadar-run-${runId}-${ts}.json`, JSON.stringify(payload, null, 2));
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const lines = (session?.lines || []).map((line) => extractLineText(line)).filter(Boolean);
|
|
1578
|
+
const outcomes = Array.isArray(run?.fileOutcomes) ? run.fileOutcomes : [];
|
|
1579
|
+
const stats = run?.validationStats || {};
|
|
1580
|
+
const md = [
|
|
1581
|
+
`# VibeRadar Run Export`,
|
|
1582
|
+
``,
|
|
1583
|
+
`- runId: ${run?.runId || runId}`,
|
|
1584
|
+
`- title: ${run?.title || session?.title || '-'}`,
|
|
1585
|
+
`- phase: ${run?.phase || '-'}`,
|
|
1586
|
+
`- exportedAt: ${new Date().toISOString()}`,
|
|
1587
|
+
``,
|
|
1588
|
+
`## Validation`,
|
|
1589
|
+
``,
|
|
1590
|
+
`- covered: ${stats.covered ?? 0}`,
|
|
1591
|
+
`- not-covered: ${stats.notCovered ?? 0}`,
|
|
1592
|
+
`- blocked: ${stats.blocked ?? 0}`,
|
|
1593
|
+
`- infra: ${stats.infra ?? 0}`,
|
|
1594
|
+
``,
|
|
1595
|
+
`## File Outcomes`,
|
|
1596
|
+
``,
|
|
1597
|
+
...outcomes.map((o) => `- ${o.status}: ${o.sourcePath}${o.testFile ? ` -> ${o.testFile}` : ''}${o.reason ? ` (${o.reason})` : ''}`),
|
|
1598
|
+
``,
|
|
1599
|
+
`## Logs`,
|
|
1600
|
+
``,
|
|
1601
|
+
'```text',
|
|
1602
|
+
...lines,
|
|
1603
|
+
'```',
|
|
1604
|
+
].join('\n');
|
|
1605
|
+
downloadTextArtifact(`viberadar-run-${runId}-${ts}.md`, md);
|
|
1606
|
+
}
|
|
1349
1607
|
|
|
1350
|
-
function renderActiveSession() {
|
|
1351
|
-
const term = document.getElementById('agentTerminal');
|
|
1352
|
-
if (!term) return;
|
|
1353
|
-
term.innerHTML = '';
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1608
|
+
function renderActiveSession() {
|
|
1609
|
+
const term = document.getElementById('agentTerminal');
|
|
1610
|
+
if (!term) return;
|
|
1611
|
+
term.innerHTML = '';
|
|
1612
|
+
terminalMatchRefs = [];
|
|
1613
|
+
terminalCommandRefs = [];
|
|
1614
|
+
terminalErrorRefs = [];
|
|
1615
|
+
terminalMatchCursor = -1;
|
|
1616
|
+
terminalCommandCursor = -1;
|
|
1617
|
+
terminalErrorCursor = -1;
|
|
1618
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1619
|
+
if (!s) {
|
|
1620
|
+
document.getElementById('agentSearchMeta').textContent = '0 matches';
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const normalizedQuery = (terminalSearchQuery || '').trim();
|
|
1624
|
+
let regex = null;
|
|
1625
|
+
if (normalizedQuery && terminalSearchRegex) {
|
|
1626
|
+
try { regex = new RegExp(normalizedQuery, 'i'); } catch { regex = null; }
|
|
1627
|
+
}
|
|
1628
|
+
for (let i = 0; i < s.lines.length; i++) {
|
|
1629
|
+
const ln = s.lines[i];
|
|
1630
|
+
const text = extractLineText(ln);
|
|
1631
|
+
const isErrorLine = !!ln.isError || /(^|\s)❌/.test(text) || /\berror\b/i.test(text);
|
|
1632
|
+
const isCommandLine = /^\s*⚡\s*\$/.test(text);
|
|
1633
|
+
if (terminalSearchCurrentRunOnly && currentRunId && s.runId && s.runId !== currentRunId) continue;
|
|
1634
|
+
if (terminalSearchErrorsOnly && !isErrorLine) continue;
|
|
1635
|
+
let isMatch = false;
|
|
1636
|
+
if (!normalizedQuery) {
|
|
1637
|
+
isMatch = false;
|
|
1638
|
+
} else if (regex) {
|
|
1639
|
+
isMatch = regex.test(text);
|
|
1640
|
+
} else {
|
|
1641
|
+
isMatch = text.toLowerCase().includes(normalizedQuery.toLowerCase());
|
|
1642
|
+
}
|
|
1643
|
+
if (normalizedQuery && !isMatch) continue;
|
|
1644
|
+
|
|
1645
|
+
const el = document.createElement('div');
|
|
1646
|
+
if (ln.html) {
|
|
1647
|
+
el.innerHTML = ln.html;
|
|
1648
|
+
} else {
|
|
1649
|
+
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1650
|
+
el.textContent = ln.text;
|
|
1651
|
+
}
|
|
1652
|
+
if (isCommandLine) el.classList.add('command');
|
|
1653
|
+
if (isMatch) el.classList.add('match');
|
|
1654
|
+
el.dataset.lineIndex = String(i);
|
|
1655
|
+
term.appendChild(el);
|
|
1656
|
+
if (isMatch) terminalMatchRefs.push(el);
|
|
1657
|
+
if (isCommandLine) terminalCommandRefs.push(el);
|
|
1658
|
+
if (isErrorLine) terminalErrorRefs.push(el);
|
|
1659
|
+
}
|
|
1660
|
+
term.scrollTop = term.scrollHeight;
|
|
1661
|
+
document.getElementById('agentSearchMeta').textContent =
|
|
1662
|
+
`${terminalMatchRefs.length} matches • ${terminalCommandRefs.length} commands • ${terminalErrorRefs.length} errors`;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function extractLineText(line) {
|
|
1666
|
+
if (!line) return '';
|
|
1667
|
+
if (line.text !== undefined) return String(line.text || '');
|
|
1668
|
+
if (line.html) {
|
|
1669
|
+
const tmp = document.createElement('div');
|
|
1670
|
+
tmp.innerHTML = line.html;
|
|
1671
|
+
return tmp.innerText || '';
|
|
1672
|
+
}
|
|
1673
|
+
return '';
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function jumpByRefs(refs, dir, cursorName) {
|
|
1677
|
+
if (!refs.length) return;
|
|
1678
|
+
if (cursorName === 'match') {
|
|
1679
|
+
terminalMatchCursor = (terminalMatchCursor + dir + refs.length) % refs.length;
|
|
1680
|
+
refs[terminalMatchCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (cursorName === 'command') {
|
|
1684
|
+
terminalCommandCursor = (terminalCommandCursor + dir + refs.length) % refs.length;
|
|
1685
|
+
refs[terminalCommandCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
terminalErrorCursor = (terminalErrorCursor + dir + refs.length) % refs.length;
|
|
1689
|
+
refs[terminalErrorCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function jumpTerminalMatch(dir) {
|
|
1693
|
+
jumpByRefs(terminalMatchRefs, dir > 0 ? 1 : -1, 'match');
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function jumpCommand(dir) {
|
|
1697
|
+
jumpByRefs(terminalCommandRefs, dir > 0 ? 1 : -1, 'command');
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function jumpError(dir) {
|
|
1701
|
+
jumpByRefs(terminalErrorRefs, dir > 0 ? 1 : -1, 'error');
|
|
1702
|
+
}
|
|
1368
1703
|
|
|
1369
1704
|
async function setAgent(agent) {
|
|
1370
1705
|
await fetch('/api/set-agent', {
|
|
@@ -1413,19 +1748,187 @@ function isFileAgentActive(relPath) {
|
|
|
1413
1748
|
return null;
|
|
1414
1749
|
}
|
|
1415
1750
|
|
|
1416
|
-
function updateQueueBadge(n) {
|
|
1417
|
-
document.getElementById('agentQueueCount').textContent = n;
|
|
1418
|
-
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1419
|
-
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1420
|
-
|
|
1751
|
+
function updateQueueBadge(n) {
|
|
1752
|
+
document.getElementById('agentQueueCount').textContent = n;
|
|
1753
|
+
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1754
|
+
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1755
|
+
renderQueuePanel();
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function renderQueuePanel() {
|
|
1759
|
+
const panel = document.getElementById('agentQueuePanel');
|
|
1760
|
+
if (!panel) return;
|
|
1761
|
+
if (!Array.isArray(agentQueueState) || agentQueueState.length === 0) {
|
|
1762
|
+
panel.style.display = 'none';
|
|
1763
|
+
panel.innerHTML = '';
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
panel.style.display = 'block';
|
|
1767
|
+
panel.innerHTML = `
|
|
1768
|
+
<div class="agent-queue-title">Очередь задач (${agentQueueState.length})</div>
|
|
1769
|
+
${agentQueueState.map((item, idx) => `
|
|
1770
|
+
<div class="agent-queue-item">
|
|
1771
|
+
<span class="agent-queue-pos">#${item.position ?? idx + 1}</span>
|
|
1772
|
+
<span title="${escapeHtml(item.title || '')}">${escapeHtml(item.title || item.task || item.runId)}</span>
|
|
1773
|
+
<span style="color:var(--dim)">(${escapeHtml(item.task || 'task')})</span>
|
|
1774
|
+
<div class="agent-queue-actions">
|
|
1775
|
+
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','up')" title="Сдвинуть вверх">↑</button>
|
|
1776
|
+
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','down')" title="Сдвинуть вниз">↓</button>
|
|
1777
|
+
<button class="agent-queue-action" onclick="cancelQueueItem('${item.runId}')" title="Отменить задачу">✕</button>
|
|
1778
|
+
<button class="agent-queue-action" onclick="retryRun('${item.runId}')" title="Retry">↻</button>
|
|
1779
|
+
</div>
|
|
1780
|
+
</div>
|
|
1781
|
+
`).join('')}
|
|
1782
|
+
`;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function renderRunSummaryMatrix(summary = lastRunSummary) {
|
|
1786
|
+
const box = document.getElementById('agentSummaryMatrix');
|
|
1787
|
+
if (!box) return;
|
|
1788
|
+
const outcomes = summary?.fileOutcomes || [];
|
|
1789
|
+
if (!Array.isArray(outcomes) || outcomes.length === 0) {
|
|
1790
|
+
box.style.display = 'none';
|
|
1791
|
+
box.innerHTML = '';
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const stats = summary?.validationStats || {};
|
|
1795
|
+
box.style.display = 'block';
|
|
1796
|
+
box.innerHTML = `
|
|
1797
|
+
<div class="agent-summary-title">
|
|
1798
|
+
Матрица итогов (run: ${escapeHtml(summary?.runId || '—')}) • covered: ${stats.covered ?? 0} • not-covered: ${stats.notCovered ?? 0} • blocked: ${stats.blocked ?? 0} • infra: ${stats.infra ?? 0}
|
|
1799
|
+
</div>
|
|
1800
|
+
${outcomes.map((entry) => `
|
|
1801
|
+
<div class="agent-summary-row">
|
|
1802
|
+
<span class="agent-summary-status-${entry.status}">${escapeHtml(entry.status)}</span>
|
|
1803
|
+
<span title="${escapeHtml(entry.sourcePath || '')}">${escapeHtml(entry.sourcePath || '')}</span>
|
|
1804
|
+
<span style="color:var(--dim)">${escapeHtml(entry.testFile || entry.reason || '')}</span>
|
|
1805
|
+
</div>
|
|
1806
|
+
`).join('')}
|
|
1807
|
+
`;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
async function cancelQueueItem(runId) {
|
|
1811
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
|
|
1812
|
+
await loadAgentState();
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
async function retryRun(runId) {
|
|
1816
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/retry`, { method: 'POST' });
|
|
1817
|
+
await loadAgentState();
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
async function reorderQueueItem(runId, direction) {
|
|
1821
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/reorder`, {
|
|
1822
|
+
method: 'POST',
|
|
1823
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1824
|
+
body: JSON.stringify({ direction }),
|
|
1825
|
+
});
|
|
1826
|
+
await loadAgentState();
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function ensureSessionForRun(run, makeActive = false) {
|
|
1830
|
+
if (!run?.runId) return null;
|
|
1831
|
+
let sessionId = runSessionMap.get(run.runId);
|
|
1832
|
+
if (!sessionId) {
|
|
1833
|
+
sessionId = createSession(run.title || `Run ${run.runId}`, run.phase === 'failed' ? 'error' : run.phase === 'completed' ? 'ok' : 'running', run.runId);
|
|
1834
|
+
const meta = [`runId: ${run.runId}`, `phase: ${run.phase}`];
|
|
1835
|
+
if (Array.isArray(run.targetSourcePaths) && run.targetSourcePaths.length > 0) {
|
|
1836
|
+
meta.push(`targets: ${run.targetSourcePaths.length}`);
|
|
1837
|
+
}
|
|
1838
|
+
appendToSession(sessionId, `ℹ ${meta.join(' • ')}`, false, true);
|
|
1839
|
+
}
|
|
1840
|
+
if (makeActive) switchSession(sessionId);
|
|
1841
|
+
return sessionId;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function applyAgentStateSnapshot(state) {
|
|
1845
|
+
if (!state) return;
|
|
1846
|
+
agentQueueState = Array.isArray(state.queue) ? state.queue : [];
|
|
1847
|
+
agentRunsState = Array.isArray(state.runs) ? state.runs : [];
|
|
1848
|
+
agentActiveRun = state.activeRun || null;
|
|
1849
|
+
updateQueueBadge(agentQueueState.length);
|
|
1850
|
+
if (agentActiveRun?.runId) {
|
|
1851
|
+
currentRunId = agentActiveRun.runId;
|
|
1852
|
+
ensureSessionForRun(agentActiveRun);
|
|
1853
|
+
setAgentRunning(['starting', 'running', 'validating'].includes(agentActiveRun.phase));
|
|
1854
|
+
} else {
|
|
1855
|
+
setAgentRunning(false);
|
|
1856
|
+
}
|
|
1857
|
+
const latestSummary = [...agentRunsState].reverse().find((r) => Array.isArray(r.fileOutcomes) && r.fileOutcomes.length > 0);
|
|
1858
|
+
if (latestSummary) {
|
|
1859
|
+
lastRunSummary = latestSummary;
|
|
1860
|
+
renderRunSummaryMatrix(latestSummary);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
async function loadAgentState() {
|
|
1865
|
+
try {
|
|
1866
|
+
const res = await fetch('/api/agent/state');
|
|
1867
|
+
if (!res.ok) return;
|
|
1868
|
+
const state = await res.json();
|
|
1869
|
+
applyAgentStateSnapshot(state);
|
|
1870
|
+
} catch {}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function upsertRunState(run) {
|
|
1874
|
+
if (!run?.runId) return;
|
|
1875
|
+
const idx = agentRunsState.findIndex((r) => r.runId === run.runId);
|
|
1876
|
+
if (idx >= 0) agentRunsState[idx] = { ...agentRunsState[idx], ...run };
|
|
1877
|
+
else agentRunsState.push(run);
|
|
1878
|
+
if (agentRunsState.length > 80) {
|
|
1879
|
+
agentRunsState = agentRunsState.slice(agentRunsState.length - 80);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function updateSessionFromRun(run) {
|
|
1884
|
+
if (!run?.runId) return;
|
|
1885
|
+
upsertRunState(run);
|
|
1886
|
+
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
1887
|
+
setAgentRunning(true);
|
|
1888
|
+
currentRunId = run.runId;
|
|
1889
|
+
const sid = ensureSessionForRun(run);
|
|
1890
|
+
if (sid) {
|
|
1891
|
+
runningSessionId = sid;
|
|
1892
|
+
if (activeSessionId === sid) {
|
|
1893
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + (run.title || 'Agent');
|
|
1894
|
+
document.getElementById('agentPanelStatus').textContent = run.phase === 'validating' ? 'проверяю…' : 'работает…';
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const sid = runSessionMap.get(run.runId);
|
|
1900
|
+
if (sid) {
|
|
1901
|
+
const status = run.phase === 'completed' ? 'ok' : run.phase === 'canceled' ? 'error' : run.phase === 'failed' ? 'error' : 'info';
|
|
1902
|
+
updateSessionStatus(sid, status);
|
|
1903
|
+
if (runningSessionId === sid) runningSessionId = null;
|
|
1904
|
+
if (activeSessionId === sid) {
|
|
1905
|
+
document.getElementById('agentPanelStatus').textContent =
|
|
1906
|
+
run.phase === 'completed' ? '✅ готово' : run.phase === 'canceled' ? '⏹ отменено' : run.phase === 'failed' ? '❌ ошибка' : run.phase;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (run.phase === 'completed' || run.phase === 'failed' || run.phase === 'canceled') {
|
|
1910
|
+
if (agentActiveRun?.runId === run.runId) agentActiveRun = null;
|
|
1911
|
+
if (currentRunId === run.runId) currentRunId = null;
|
|
1912
|
+
setAgentRunning(false);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function refreshPathActivityFromState() {
|
|
1917
|
+
agentQueuedPaths.clear();
|
|
1918
|
+
(agentQueueState || []).forEach((q) => {
|
|
1919
|
+
getTaskFilePaths(q.task, q.featureKey, q.filePath, q.selectedFilePaths).forEach((p) => agentQueuedPaths.add(p));
|
|
1920
|
+
});
|
|
1921
|
+
if (!agentActiveRun || !['starting', 'running', 'validating'].includes(agentActiveRun.phase)) {
|
|
1922
|
+
agentRunningPaths.clear();
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1421
1925
|
|
|
1422
|
-
async function cancelAgent() {
|
|
1423
|
-
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1926
|
+
async function cancelAgent() {
|
|
1927
|
+
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1928
|
+
await loadAgentState();
|
|
1929
|
+
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
1930
|
+
if (runningSessionId) {
|
|
1931
|
+
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1429
1932
|
updateSessionStatus(runningSessionId, 'error');
|
|
1430
1933
|
runningSessionId = null;
|
|
1431
1934
|
} else {
|
|
@@ -1433,11 +1936,11 @@ async function cancelAgent() {
|
|
|
1433
1936
|
}
|
|
1434
1937
|
}
|
|
1435
1938
|
|
|
1436
|
-
async function clearAgentQueue() {
|
|
1437
|
-
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1438
|
-
|
|
1439
|
-
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1440
|
-
}
|
|
1939
|
+
async function clearAgentQueue() {
|
|
1940
|
+
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1941
|
+
await loadAgentState();
|
|
1942
|
+
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1943
|
+
}
|
|
1441
1944
|
|
|
1442
1945
|
// ─── File row more menu ──────────────────────────────────────────────────────
|
|
1443
1946
|
let _openFileMenu = null;
|
|
@@ -2319,16 +2822,20 @@ function renderFeatureDetail(c) {
|
|
|
2319
2822
|
? `${meta.icon} ${meta.label} тесты (${listFiles.length})`
|
|
2320
2823
|
: `📁 Файлы фичи (${listFiles.length})`;
|
|
2321
2824
|
|
|
2322
|
-
const q = searchQuery.toLowerCase();
|
|
2323
|
-
const filtered = q ? listFiles.filter(m =>
|
|
2324
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2325
|
-
) : listFiles;
|
|
2326
|
-
const
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2825
|
+
const q = searchQuery.toLowerCase();
|
|
2826
|
+
const filtered = q ? listFiles.filter(m =>
|
|
2827
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2828
|
+
) : listFiles;
|
|
2829
|
+
const rowsRenderKey = `feature:${drillFeatureKey}:${activeTab}:${showOnlyUntestedInFeature ? 1 : 0}:${q}`;
|
|
2830
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2831
|
+
const selectableSource = !isTestList && !!D.agent;
|
|
2832
|
+
const visibleSourcePaths = selectableSource
|
|
2833
|
+
? visibleRows.map(m => m.relativePath.replace(/\\/g, '/'))
|
|
2834
|
+
: [];
|
|
2835
|
+
const selectedVisible = selectableSource
|
|
2836
|
+
? selectedFilesForFeature(drillFeatureKey, visibleRows.map(m => m.relativePath))
|
|
2837
|
+
: [];
|
|
2838
|
+
const selectedCount = selectedVisible.length;
|
|
2332
2839
|
|
|
2333
2840
|
c.innerHTML = `
|
|
2334
2841
|
<div class="drill-header">
|
|
@@ -2381,9 +2888,14 @@ function renderFeatureDetail(c) {
|
|
|
2381
2888
|
? 'Все файлы этой фичи уже покрыты тестами'
|
|
2382
2889
|
: 'Нет файлов — возможно паттерны в конфиге не совпадают')}
|
|
2383
2890
|
</div>`
|
|
2384
|
-
:
|
|
2891
|
+
: visibleRows.map(m => fileRow(m, isTestList, drillFeatureKey, selectableSource)).join('')
|
|
2385
2892
|
}
|
|
2386
|
-
</div
|
|
2893
|
+
</div>
|
|
2894
|
+
${hasMoreRows ? `
|
|
2895
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
2896
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
2897
|
+
<button class="bulk-actions-btn" id="fileRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
2898
|
+
</div>` : ''}`;
|
|
2387
2899
|
|
|
2388
2900
|
const untestedOnlyToggle = document.getElementById('untestedOnlyToggle');
|
|
2389
2901
|
if (untestedOnlyToggle) {
|
|
@@ -2428,9 +2940,17 @@ function renderFeatureDetail(c) {
|
|
|
2428
2940
|
renderContent();
|
|
2429
2941
|
};
|
|
2430
2942
|
}
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
const fileRowsLoadMoreBtn = document.getElementById('fileRowsLoadMoreBtn');
|
|
2946
|
+
if (fileRowsLoadMoreBtn) {
|
|
2947
|
+
fileRowsLoadMoreBtn.onclick = () => {
|
|
2948
|
+
increaseFileRowsLimit();
|
|
2949
|
+
renderContent();
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
2434
2954
|
card.onclick = async () => {
|
|
2435
2955
|
const type = card.dataset.testtype;
|
|
2436
2956
|
drillTestType = (type === 'source') ? null : type; // 'source' tab = null state
|
|
@@ -2439,17 +2959,11 @@ function renderFeatureDetail(c) {
|
|
|
2439
2959
|
if (type === 'e2e') {
|
|
2440
2960
|
await loadE2ePlan(drillFeatureKey);
|
|
2441
2961
|
}
|
|
2442
|
-
renderContent();
|
|
2443
|
-
};
|
|
2444
|
-
});
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
row.onclick = () => {
|
|
2448
|
-
const m = D.modules.find(m => m.id === row.dataset.id);
|
|
2449
|
-
if (m) openModulePanel(m);
|
|
2450
|
-
};
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2962
|
+
renderContent();
|
|
2963
|
+
};
|
|
2964
|
+
});
|
|
2965
|
+
bindFileRowsClick(c);
|
|
2966
|
+
}
|
|
2453
2967
|
|
|
2454
2968
|
const TEST_TYPE_META = {
|
|
2455
2969
|
unit: { label: 'Unit', icon: '🧪', color: '#e3b341', desc: 'Изолированные тесты функций и модулей' },
|
|
@@ -2468,10 +2982,12 @@ function renderTestTypeDetail(c) {
|
|
|
2468
2982
|
m.featureKeys && m.featureKeys.includes(drillFeatureKey)
|
|
2469
2983
|
);
|
|
2470
2984
|
|
|
2471
|
-
const q = searchQuery.toLowerCase();
|
|
2472
|
-
const filtered = q ? tests.filter(m =>
|
|
2473
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2474
|
-
) : tests;
|
|
2985
|
+
const q = searchQuery.toLowerCase();
|
|
2986
|
+
const filtered = q ? tests.filter(m =>
|
|
2987
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2988
|
+
) : tests;
|
|
2989
|
+
const rowsRenderKey = `tests:${drillFeatureKey}:${drillTestType}:${q}`;
|
|
2990
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2475
2991
|
|
|
2476
2992
|
c.innerHTML = `
|
|
2477
2993
|
<div class="drill-header">
|
|
@@ -2486,24 +3002,32 @@ function renderTestTypeDetail(c) {
|
|
|
2486
3002
|
</div>
|
|
2487
3003
|
<div style="font-size:12px;color:var(--dim);margin-bottom:16px">${meta.desc}</div>
|
|
2488
3004
|
|
|
2489
|
-
<div class="file-rows" id="fileRows">
|
|
2490
|
-
${filtered.length === 0
|
|
2491
|
-
? `<div style="padding:24px;text-align:center;border:1px dashed var(--border);border-radius:8px">
|
|
2492
|
-
<div style="font-size:28px;margin-bottom:8px">${meta.icon}</div>
|
|
2493
|
-
<div style="font-size:14px;color:var(--muted);margin-bottom:4px">Нет ${meta.label} тестов для этой фичи</div>
|
|
2494
|
-
<div style="font-size:12px;color:var(--dim)">Добавь тесты в <code>${drillTestType === 'e2e' ? 'e2e/' : 'tests/'}</code></div>
|
|
2495
|
-
</div>`
|
|
2496
|
-
:
|
|
2497
|
-
}
|
|
2498
|
-
</div
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
3005
|
+
<div class="file-rows" id="fileRows">
|
|
3006
|
+
${filtered.length === 0
|
|
3007
|
+
? `<div style="padding:24px;text-align:center;border:1px dashed var(--border);border-radius:8px">
|
|
3008
|
+
<div style="font-size:28px;margin-bottom:8px">${meta.icon}</div>
|
|
3009
|
+
<div style="font-size:14px;color:var(--muted);margin-bottom:4px">Нет ${meta.label} тестов для этой фичи</div>
|
|
3010
|
+
<div style="font-size:12px;color:var(--dim)">Добавь тесты в <code>${drillTestType === 'e2e' ? 'e2e/' : 'tests/'}</code></div>
|
|
3011
|
+
</div>`
|
|
3012
|
+
: visibleRows.map(m => fileRow(m, true)).join('')
|
|
3013
|
+
}
|
|
3014
|
+
</div>
|
|
3015
|
+
${hasMoreRows ? `
|
|
3016
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3017
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} тест-файлов</span>
|
|
3018
|
+
<button class="bulk-actions-btn" id="testRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3019
|
+
</div>` : ''}`;
|
|
3020
|
+
|
|
3021
|
+
const testRowsLoadMoreBtn = document.getElementById('testRowsLoadMoreBtn');
|
|
3022
|
+
if (testRowsLoadMoreBtn) {
|
|
3023
|
+
testRowsLoadMoreBtn.onclick = () => {
|
|
3024
|
+
increaseFileRowsLimit();
|
|
3025
|
+
renderContent();
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
bindFileRowsClick(c);
|
|
3030
|
+
}
|
|
2507
3031
|
|
|
2508
3032
|
function renderUnmappedDetail(c) {
|
|
2509
3033
|
const infraSrc = D.modules.filter(m => m.type !== 'test' && m.isInfra);
|
|
@@ -2511,10 +3035,12 @@ function renderUnmappedDetail(c) {
|
|
|
2511
3035
|
m.type !== 'test' && !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)
|
|
2512
3036
|
);
|
|
2513
3037
|
|
|
2514
|
-
const q = searchQuery.toLowerCase();
|
|
2515
|
-
const filtered = q ? unmappedSrc.filter(m =>
|
|
2516
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2517
|
-
) : unmappedSrc;
|
|
3038
|
+
const q = searchQuery.toLowerCase();
|
|
3039
|
+
const filtered = q ? unmappedSrc.filter(m =>
|
|
3040
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3041
|
+
) : unmappedSrc;
|
|
3042
|
+
const rowsRenderKey = `unmapped:${q}`;
|
|
3043
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2518
3044
|
|
|
2519
3045
|
// Build prompt text
|
|
2520
3046
|
const featureList = (D.features || []).map(f => ` • ${f.key} — ${f.label}`).join('\n');
|
|
@@ -2555,36 +3081,44 @@ function renderUnmappedDetail(c) {
|
|
|
2555
3081
|
border-radius:6px; color:var(--blue); font-size:12px; cursor:pointer;
|
|
2556
3082
|
">📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)</button>
|
|
2557
3083
|
</div>
|
|
2558
|
-
<div class="file-rows" id="fileRows">
|
|
2559
|
-
${filtered.length === 0
|
|
2560
|
-
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
2561
|
-
:
|
|
2562
|
-
}
|
|
2563
|
-
</div
|
|
3084
|
+
<div class="file-rows" id="fileRows">
|
|
3085
|
+
${filtered.length === 0
|
|
3086
|
+
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
3087
|
+
: visibleRows.map(m => fileRow(m)).join('')
|
|
3088
|
+
}
|
|
3089
|
+
</div>
|
|
3090
|
+
${hasMoreRows ? `
|
|
3091
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3092
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
3093
|
+
<button class="bulk-actions-btn" id="unmappedRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3094
|
+
</div>` : ''}`;
|
|
2564
3095
|
|
|
2565
3096
|
const runAgentUnmappedBtn = document.getElementById('runAgentUnmapped');
|
|
2566
3097
|
if (runAgentUnmappedBtn) {
|
|
2567
3098
|
runAgentUnmappedBtn.onclick = () => runAgentTask('map-unmapped');
|
|
2568
3099
|
}
|
|
2569
3100
|
|
|
2570
|
-
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
3101
|
+
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
2571
3102
|
navigator.clipboard.writeText(promptText).then(() => {
|
|
2572
3103
|
this.textContent = '✅ Скопировано!';
|
|
2573
3104
|
this.style.color = 'var(--green)';
|
|
2574
3105
|
setTimeout(() => {
|
|
2575
3106
|
this.textContent = `📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)`;
|
|
2576
3107
|
this.style.color = 'var(--blue)';
|
|
2577
|
-
}, 3000);
|
|
2578
|
-
});
|
|
2579
|
-
};
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
}
|
|
3108
|
+
}, 3000);
|
|
3109
|
+
});
|
|
3110
|
+
};
|
|
3111
|
+
|
|
3112
|
+
const unmappedRowsLoadMoreBtn = document.getElementById('unmappedRowsLoadMoreBtn');
|
|
3113
|
+
if (unmappedRowsLoadMoreBtn) {
|
|
3114
|
+
unmappedRowsLoadMoreBtn.onclick = () => {
|
|
3115
|
+
increaseFileRowsLimit();
|
|
3116
|
+
renderContent();
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
bindFileRowsClick(c);
|
|
3121
|
+
}
|
|
2588
3122
|
|
|
2589
3123
|
function fileRow(m, isTest = false, featureKey = null, selectable = false) {
|
|
2590
3124
|
const relPath = m.relativePath.replace(/\\/g, '/');
|
|
@@ -2674,23 +3208,33 @@ function toggleSourceSelection(relPath, checked) {
|
|
|
2674
3208
|
renderContent();
|
|
2675
3209
|
}
|
|
2676
3210
|
|
|
2677
|
-
function renderModuleGrid(c) {
|
|
2678
|
-
const q = searchQuery.toLowerCase();
|
|
2679
|
-
const list = D.modules.filter(m => {
|
|
2680
|
-
if (activeTypes.size > 0 && !activeTypes.has(m.type)) return false;
|
|
2681
|
-
if (q && !m.name.toLowerCase().includes(q) && !m.relativePath.toLowerCase().includes(q)) return false;
|
|
2682
|
-
return true;
|
|
2683
|
-
});
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
3211
|
+
function renderModuleGrid(c) {
|
|
3212
|
+
const q = searchQuery.toLowerCase();
|
|
3213
|
+
const list = D.modules.filter(m => {
|
|
3214
|
+
if (activeTypes.size > 0 && !activeTypes.has(m.type)) return false;
|
|
3215
|
+
if (q && !m.name.toLowerCase().includes(q) && !m.relativePath.toLowerCase().includes(q)) return false;
|
|
3216
|
+
return true;
|
|
3217
|
+
});
|
|
3218
|
+
const typeKey = [...activeTypes].sort().join(',');
|
|
3219
|
+
const rowsRenderKey = `modules:${contextMode}:${view}:${typeKey}:${q}`;
|
|
3220
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(list, rowsRenderKey);
|
|
3221
|
+
|
|
3222
|
+
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
3223
|
+
|
|
3224
|
+
c.innerHTML = `
|
|
3225
|
+
<div class="module-grid" id="modGrid"></div>
|
|
3226
|
+
${hasMoreRows ? `
|
|
3227
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3228
|
+
<span>Показано ${visibleRows.length} из ${list.length} модулей</span>
|
|
3229
|
+
<button class="bulk-actions-btn" id="moduleRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3230
|
+
</div>` : ''}
|
|
3231
|
+
`;
|
|
3232
|
+
const grid = document.getElementById('modGrid');
|
|
3233
|
+
|
|
3234
|
+
visibleRows.forEach(m => {
|
|
3235
|
+
const cov = m.coverage?.lines;
|
|
3236
|
+
const isActive = activePanelKey === m.id;
|
|
3237
|
+
const card = document.createElement('div');
|
|
2694
3238
|
card.className = 'module-card' + (isActive ? ' active' : '');
|
|
2695
3239
|
card.innerHTML = `
|
|
2696
3240
|
<div class="module-name">${m.name}</div>
|
|
@@ -2702,10 +3246,18 @@ function renderModuleGrid(c) {
|
|
|
2702
3246
|
<span class="badge ${m.hasTests ? 'badge-green' : 'badge-red'}">${m.hasTests ? '✓' : '✗'}</span>
|
|
2703
3247
|
</div>
|
|
2704
3248
|
${cov != null ? `<div class="cov-bar"><div class="cov-fill" style="width:${cov}%;background:${covColor(cov)}"></div></div>` : ''}`;
|
|
2705
|
-
card.onclick = () => openModulePanel(m);
|
|
2706
|
-
grid.appendChild(card);
|
|
2707
|
-
});
|
|
2708
|
-
|
|
3249
|
+
card.onclick = () => openModulePanel(m);
|
|
3250
|
+
grid.appendChild(card);
|
|
3251
|
+
});
|
|
3252
|
+
|
|
3253
|
+
const moduleRowsLoadMoreBtn = document.getElementById('moduleRowsLoadMoreBtn');
|
|
3254
|
+
if (moduleRowsLoadMoreBtn) {
|
|
3255
|
+
moduleRowsLoadMoreBtn.onclick = () => {
|
|
3256
|
+
increaseFileRowsLimit();
|
|
3257
|
+
renderContent();
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
2709
3261
|
|
|
2710
3262
|
// ─── Panels ───────────────────────────────────────────────────────────────────
|
|
2711
3263
|
function openFeaturePanel(key) {
|
|
@@ -2916,14 +3468,29 @@ function openUnmappedPanel(files, infraFiles) {
|
|
|
2916
3468
|
document.getElementById('panel').classList.add('open');
|
|
2917
3469
|
}
|
|
2918
3470
|
|
|
2919
|
-
function closePanel() {
|
|
2920
|
-
activePanelKey = null;
|
|
2921
|
-
document.getElementById('panel').classList.remove('open');
|
|
2922
|
-
renderContent();
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
document.
|
|
3471
|
+
function closePanel() {
|
|
3472
|
+
activePanelKey = null;
|
|
3473
|
+
document.getElementById('panel').classList.remove('open');
|
|
3474
|
+
renderContent();
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
function applyTerminalFiltersFromUi() {
|
|
3478
|
+
terminalSearchQuery = document.getElementById('agentSearchInput')?.value || '';
|
|
3479
|
+
terminalSearchErrorsOnly = !!document.getElementById('agentSearchErrorsOnly')?.checked;
|
|
3480
|
+
terminalSearchCurrentRunOnly = !!document.getElementById('agentSearchCurrentRunOnly')?.checked;
|
|
3481
|
+
terminalSearchRegex = !!document.getElementById('agentSearchRegex')?.checked;
|
|
3482
|
+
renderActiveSession();
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
['agentSearchInput', 'agentSearchErrorsOnly', 'agentSearchCurrentRunOnly', 'agentSearchRegex'].forEach((id) => {
|
|
3486
|
+
const el = document.getElementById(id);
|
|
3487
|
+
if (!el) return;
|
|
3488
|
+
const eventName = id === 'agentSearchInput' ? 'input' : 'change';
|
|
3489
|
+
el.addEventListener(eventName, applyTerminalFiltersFromUi);
|
|
3490
|
+
});
|
|
3491
|
+
|
|
3492
|
+
// ─── Events ───────────────────────────────────────────────────────────────────
|
|
3493
|
+
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
2927
3494
|
tab.onclick = () => {
|
|
2928
3495
|
if (contextMode !== 'qa') return;
|
|
2929
3496
|
if (tab.classList.contains('disabled')) return;
|
|
@@ -2973,129 +3540,204 @@ window.addEventListener('popstate', () => {
|
|
|
2973
3540
|
});
|
|
2974
3541
|
|
|
2975
3542
|
// ─── Live reload ──────────────────────────────────────────────────────────────
|
|
2976
|
-
function setLiveDot(color, title) {
|
|
2977
|
-
const dot = document.getElementById('liveDot');
|
|
2978
|
-
dot.style.background = color;
|
|
2979
|
-
dot.title = title;
|
|
2980
|
-
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3543
|
+
function setLiveDot(color, title) {
|
|
3544
|
+
const dot = document.getElementById('liveDot');
|
|
3545
|
+
dot.style.background = color;
|
|
3546
|
+
dot.title = title;
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
function scheduleRefreshData(delayMs = 120) {
|
|
3550
|
+
if (refreshDataTimer) return;
|
|
3551
|
+
refreshDataTimer = setTimeout(() => {
|
|
3552
|
+
refreshDataTimer = null;
|
|
3553
|
+
void refreshData();
|
|
3554
|
+
}, delayMs);
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
async function refreshData() {
|
|
3558
|
+
if (refreshDataInFlight) {
|
|
3559
|
+
refreshDataQueued = true;
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
refreshDataInFlight = true;
|
|
3563
|
+
try {
|
|
3564
|
+
const res = await fetch('/api/data');
|
|
3565
|
+
D = await res.json();
|
|
2986
3566
|
|
|
2987
3567
|
// Update header timestamp
|
|
2988
3568
|
document.getElementById('scannedAt').textContent =
|
|
2989
3569
|
new Date(D.scannedAt).toLocaleTimeString();
|
|
2990
|
-
|
|
2991
|
-
renderStats();
|
|
2992
|
-
renderSidebar();
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
3570
|
+
|
|
3571
|
+
renderStats();
|
|
3572
|
+
renderSidebar();
|
|
3573
|
+
updateAgentBtn();
|
|
3574
|
+
updateAgentRightsInfo();
|
|
3575
|
+
|
|
3576
|
+
// Re-render drill-down or re-open panel
|
|
3577
|
+
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
2999
3578
|
if (panelOpen && activePanelKey) {
|
|
3000
3579
|
if (drillFeatureKey === '__unmapped__') {
|
|
3001
3580
|
renderContent(); // already routes to renderUnmappedDetail
|
|
3002
3581
|
} else if (view === 'features' && D.features) {
|
|
3003
3582
|
openFeaturePanel(activePanelKey);
|
|
3004
|
-
} else {
|
|
3005
|
-
const m = D.modules.find(m => m.id === activePanelKey);
|
|
3006
|
-
if (m) openModulePanel(m);
|
|
3007
|
-
else closePanel();
|
|
3008
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
});
|
|
3028
|
-
|
|
3029
|
-
es.addEventListener('agent-queued', (e) => {
|
|
3030
|
-
const { queueLength, title, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3031
|
-
updateQueueBadge(queueLength);
|
|
3032
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
3033
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
3034
|
-
// Track queued paths for spinner
|
|
3035
|
-
getTaskFilePaths(task, featureKey, filePath, selectedFilePaths).forEach(p => agentQueuedPaths.add(p));
|
|
3036
|
-
renderContent();
|
|
3037
|
-
// Append queue notification to the currently running session (or active)
|
|
3038
|
-
const targetId = runningSessionId || activeSessionId;
|
|
3039
|
-
if (targetId) {
|
|
3040
|
-
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
3041
|
-
}
|
|
3042
|
-
});
|
|
3583
|
+
} else {
|
|
3584
|
+
const m = D.modules.find(m => m.id === activePanelKey);
|
|
3585
|
+
if (m) openModulePanel(m);
|
|
3586
|
+
else closePanel();
|
|
3587
|
+
}
|
|
3588
|
+
} else {
|
|
3589
|
+
renderContent();
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// Brief green flash on the dot to signal fresh data
|
|
3593
|
+
setLiveDot('#3fb950', 'Live — обновлено только что');
|
|
3594
|
+
setTimeout(() => setLiveDot('#3fb950', 'Live — автообновление включено'), 1500);
|
|
3595
|
+
void loadAgentState();
|
|
3596
|
+
} catch (err) {
|
|
3597
|
+
console.error('Refresh failed:', err);
|
|
3598
|
+
} finally {
|
|
3599
|
+
refreshDataInFlight = false;
|
|
3600
|
+
if (refreshDataQueued) {
|
|
3601
|
+
refreshDataQueued = false;
|
|
3602
|
+
scheduleRefreshData(120);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3043
3606
|
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
renderContent();
|
|
3052
|
-
// Close previous session (queue case: agent-done not fired between tasks)
|
|
3053
|
-
if (runningSessionId) {
|
|
3054
|
-
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
3055
|
-
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
3056
|
-
}
|
|
3057
|
-
const id = createSession(title);
|
|
3058
|
-
runningSessionId = id;
|
|
3059
|
-
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
3060
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
3061
|
-
});
|
|
3607
|
+
function connectSSE() {
|
|
3608
|
+
const es = new EventSource('/api/events');
|
|
3609
|
+
|
|
3610
|
+
es.onopen = () => {
|
|
3611
|
+
setLiveDot('#3fb950', 'Live — автообновление включено');
|
|
3612
|
+
void loadAgentState();
|
|
3613
|
+
};
|
|
3062
3614
|
|
|
3063
|
-
es.addEventListener('
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3615
|
+
es.addEventListener('data-updated', () => {
|
|
3616
|
+
setLiveDot('#e3b341', 'Обновляю данные…');
|
|
3617
|
+
scheduleRefreshData();
|
|
3618
|
+
});
|
|
3619
|
+
|
|
3620
|
+
es.addEventListener('agent-queue-updated', (e) => {
|
|
3621
|
+
const payload = JSON.parse(e.data || '{}');
|
|
3622
|
+
agentQueueState = Array.isArray(payload.queue) ? payload.queue : [];
|
|
3623
|
+
refreshPathActivityFromState();
|
|
3624
|
+
updateQueueBadge(agentQueueState.length);
|
|
3625
|
+
renderContent();
|
|
3626
|
+
});
|
|
3627
|
+
|
|
3628
|
+
es.addEventListener('agent-run-created', (e) => {
|
|
3629
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3630
|
+
upsertRunState(run);
|
|
3631
|
+
});
|
|
3632
|
+
|
|
3633
|
+
es.addEventListener('agent-run-updated', (e) => {
|
|
3634
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3635
|
+
if (!run) return;
|
|
3636
|
+
if (run.runId && agentQueueState.length > 0) {
|
|
3637
|
+
agentQueueState = agentQueueState.filter((q) => q.runId !== run.runId || run.phase === 'queued');
|
|
3638
|
+
updateQueueBadge(agentQueueState.length);
|
|
3639
|
+
}
|
|
3640
|
+
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
3641
|
+
agentActiveRun = run;
|
|
3642
|
+
const startedPaths = getTaskFilePaths(run.task, run.featureKey, run.filePath, run.selectedFilePaths);
|
|
3643
|
+
startedPaths.forEach((p) => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3644
|
+
renderContent();
|
|
3645
|
+
}
|
|
3646
|
+
updateSessionFromRun(run);
|
|
3647
|
+
});
|
|
3648
|
+
|
|
3649
|
+
es.addEventListener('agent-run-finished', (e) => {
|
|
3650
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3651
|
+
if (!run) return;
|
|
3652
|
+
updateSessionFromRun(run);
|
|
3653
|
+
if (Array.isArray(run.fileOutcomes) && run.fileOutcomes.length > 0) {
|
|
3654
|
+
lastRunSummary = run;
|
|
3655
|
+
renderRunSummaryMatrix(run);
|
|
3656
|
+
}
|
|
3657
|
+
refreshPathActivityFromState();
|
|
3658
|
+
renderContent();
|
|
3659
|
+
});
|
|
3660
|
+
|
|
3661
|
+
es.addEventListener('agent-queued', (e) => {
|
|
3662
|
+
const { runId, queueLength, title, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3663
|
+
updateQueueBadge(queueLength);
|
|
3664
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
3665
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
3666
|
+
// Track queued paths for spinner
|
|
3667
|
+
getTaskFilePaths(task, featureKey, filePath, selectedFilePaths).forEach(p => agentQueuedPaths.add(p));
|
|
3668
|
+
renderContent();
|
|
3669
|
+
// Append queue notification to the currently running session (or active)
|
|
3670
|
+
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3671
|
+
if (targetId) {
|
|
3672
|
+
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
3673
|
+
}
|
|
3674
|
+
void loadAgentState();
|
|
3675
|
+
});
|
|
3676
|
+
|
|
3677
|
+
es.addEventListener('agent-started', (e) => {
|
|
3678
|
+
setAgentRunning(true);
|
|
3679
|
+
const { runId, title, queueLength = 0, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3680
|
+
updateQueueBadge(queueLength);
|
|
3681
|
+
currentRunId = runId || null;
|
|
3682
|
+
// Move paths from queued → running (current task)
|
|
3683
|
+
const startedPaths = getTaskFilePaths(task, featureKey, filePath, selectedFilePaths);
|
|
3684
|
+
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3685
|
+
renderContent();
|
|
3686
|
+
// Close previous session (queue case: agent-done not fired between tasks)
|
|
3687
|
+
if (runningSessionId) {
|
|
3688
|
+
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
3689
|
+
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
3690
|
+
}
|
|
3691
|
+
const id = runId ? ensureSessionForRun({ runId, title, phase: 'running' }, true) : createSession(title);
|
|
3692
|
+
runningSessionId = id;
|
|
3693
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
3694
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
es.addEventListener('agent-output', (e) => {
|
|
3698
|
+
const { runId, line, isError, isDim } = JSON.parse(e.data);
|
|
3699
|
+
let targetId = runningSessionId;
|
|
3700
|
+
if (runId) {
|
|
3701
|
+
targetId = runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'running' });
|
|
3702
|
+
if (targetId) runningSessionId = targetId;
|
|
3703
|
+
}
|
|
3704
|
+
if (targetId) {
|
|
3705
|
+
appendToSession(targetId, line, !!isError, !!isDim);
|
|
3706
|
+
if (activeSessionId === targetId) {
|
|
3707
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
3708
|
+
}
|
|
3709
|
+
} else {
|
|
3710
|
+
appendTerminalLine(line, !!isError, !!isDim);
|
|
3711
|
+
}
|
|
3073
3712
|
});
|
|
3074
3713
|
|
|
3075
|
-
es.addEventListener('agent-done', () => {
|
|
3076
|
-
setAgentRunning(false);
|
|
3077
|
-
updateQueueBadge(0);
|
|
3078
|
-
agentRunningPaths.clear();
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
}
|
|
3714
|
+
es.addEventListener('agent-done', () => {
|
|
3715
|
+
setAgentRunning(false);
|
|
3716
|
+
if (agentQueueState.length === 0) updateQueueBadge(0);
|
|
3717
|
+
agentRunningPaths.clear();
|
|
3718
|
+
if (runningSessionId) {
|
|
3719
|
+
updateSessionStatus(runningSessionId, 'ok');
|
|
3720
|
+
if (activeSessionId === runningSessionId) {
|
|
3721
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
3722
|
+
}
|
|
3085
3723
|
runningSessionId = null;
|
|
3086
3724
|
}
|
|
3087
3725
|
renderContent();
|
|
3088
3726
|
});
|
|
3089
3727
|
|
|
3090
|
-
es.addEventListener('agent-summary', (e) => {
|
|
3091
|
-
const {
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3728
|
+
es.addEventListener('agent-summary', (e) => {
|
|
3729
|
+
const {
|
|
3730
|
+
runId,
|
|
3731
|
+
fileOutcomes = [],
|
|
3732
|
+
validationStats = null,
|
|
3733
|
+
passed = 0, failed = 0, files = [],
|
|
3734
|
+
testedFileCount = files.length,
|
|
3735
|
+
passedFileCount = Math.max(0, testedFileCount - (failed > 0 ? 1 : 0)),
|
|
3736
|
+
failedFileCount = failed > 0 ? 1 : 0,
|
|
3737
|
+
autoFixQueued = false,
|
|
3738
|
+
} = JSON.parse(e.data);
|
|
3739
|
+
const coverageOk = !validationStats || ((validationStats.notCovered || 0) === 0 && (validationStats.blocked || 0) === 0);
|
|
3740
|
+
const allOk = failed === 0 && coverageOk;
|
|
3099
3741
|
const box = document.createElement('div');
|
|
3100
3742
|
box.style.cssText = `
|
|
3101
3743
|
margin: 10px 0 4px;
|
|
@@ -3112,32 +3754,37 @@ function connectSSE() {
|
|
|
3112
3754
|
<div style="font-size:11px;color:var(--muted);margin-top:4px">
|
|
3113
3755
|
Файлы: ${testedFileCount} • passed: ${passedFileCount} • failed: ${failedFileCount}
|
|
3114
3756
|
</div>
|
|
3115
|
-
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
3116
|
-
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
3117
|
-
</div>
|
|
3118
|
-
${
|
|
3119
|
-
${
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
if (
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3757
|
+
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
3758
|
+
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
3759
|
+
</div>
|
|
3760
|
+
${validationStats ? `<div style="font-size:11px;color:var(--muted);margin-top:2px">Coverage matrix: covered ${validationStats.covered || 0} • not-covered ${validationStats.notCovered || 0} • blocked ${validationStats.blocked || 0}</div>` : ''}
|
|
3761
|
+
${autoFixQueued ? `<div style="font-size:11px;color:var(--yellow);margin-top:4px">🛠️ Автоисправление поставлено в очередь</div>` : ''}
|
|
3762
|
+
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
3763
|
+
`;
|
|
3764
|
+
if (fileOutcomes.length > 0) {
|
|
3765
|
+
lastRunSummary = { runId, fileOutcomes, validationStats };
|
|
3766
|
+
renderRunSummaryMatrix(lastRunSummary);
|
|
3767
|
+
}
|
|
3768
|
+
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3769
|
+
if (targetId) {
|
|
3770
|
+
appendToSession(targetId, box);
|
|
3771
|
+
// Mark session status from test results immediately
|
|
3772
|
+
if (runningSessionId === targetId) {
|
|
3773
|
+
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
3127
3774
|
}
|
|
3128
3775
|
}
|
|
3129
3776
|
});
|
|
3130
3777
|
|
|
3131
|
-
es.addEventListener('agent-error', (e) => {
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
// Build error node (plain text or auth/install box)
|
|
3139
|
-
let node = null;
|
|
3140
|
-
if (authRequired || notInstalled) {
|
|
3778
|
+
es.addEventListener('agent-error', (e) => {
|
|
3779
|
+
const { runId, message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
3780
|
+
if (!runId) setAgentRunning(false);
|
|
3781
|
+
agentRunningPaths.clear();
|
|
3782
|
+
agentQueuedPaths.clear();
|
|
3783
|
+
renderContent();
|
|
3784
|
+
|
|
3785
|
+
// Build error node (plain text or auth/install box)
|
|
3786
|
+
let node = null;
|
|
3787
|
+
if (authRequired || notInstalled) {
|
|
3141
3788
|
node = document.createElement('div');
|
|
3142
3789
|
node.style.cssText = 'margin:10px 0 4px;padding:10px 14px;border-radius:8px;border:1px solid var(--red);background:#2a0d0d;font-family:inherit;';
|
|
3143
3790
|
node.innerHTML = `
|
|
@@ -3145,14 +3792,14 @@ function connectSSE() {
|
|
|
3145
3792
|
${authRequired ? `<button onclick="reauthAgent()" style="margin-top:8px;padding:4px 12px;font-size:12px;background:none;border:1px solid var(--yellow);color:var(--yellow);border-radius:4px;cursor:pointer;">🔑 Перелогиниться</button>` : ''}
|
|
3146
3793
|
${notInstalled ? `<div style="margin-top:6px;font-size:11px;color:var(--dim)">После установки перезапусти viberadar</div>` : ''}
|
|
3147
3794
|
`;
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
// If no session exists yet (startup check fires before any run), create one
|
|
3151
|
-
const targetId = runningSessionId || (() => {
|
|
3152
|
-
if (authRequired || notInstalled) {
|
|
3153
|
-
const id = createSession('⚠️ Проверка агента', 'error');
|
|
3154
|
-
return id;
|
|
3155
|
-
}
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
// If no session exists yet (startup check fires before any run), create one
|
|
3798
|
+
const targetId = (runId ? (runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'failed' })) : runningSessionId) || (() => {
|
|
3799
|
+
if (authRequired || notInstalled) {
|
|
3800
|
+
const id = createSession('⚠️ Проверка агента', 'error');
|
|
3801
|
+
return id;
|
|
3802
|
+
}
|
|
3156
3803
|
return activeSessionId;
|
|
3157
3804
|
})();
|
|
3158
3805
|
|
|
@@ -3163,12 +3810,12 @@ function connectSSE() {
|
|
|
3163
3810
|
appendToSession(targetId, '❌ ' + (message || 'Ошибка агента'), true);
|
|
3164
3811
|
}
|
|
3165
3812
|
updateSessionStatus(targetId, 'error');
|
|
3166
|
-
if (activeSessionId === targetId) {
|
|
3167
|
-
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
|
-
if (runningSessionId) runningSessionId = null;
|
|
3171
|
-
});
|
|
3813
|
+
if (activeSessionId === targetId) {
|
|
3814
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
if (!runId && runningSessionId) runningSessionId = null;
|
|
3818
|
+
});
|
|
3172
3819
|
|
|
3173
3820
|
es.addEventListener('tests-started', (e) => {
|
|
3174
3821
|
const { testType, count } = JSON.parse(e.data);
|
|
@@ -3236,21 +3883,23 @@ function connectSSE() {
|
|
|
3236
3883
|
}
|
|
3237
3884
|
});
|
|
3238
3885
|
|
|
3239
|
-
es.onerror = () => {
|
|
3240
|
-
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
3241
|
-
es.close();
|
|
3242
|
-
setTimeout(connectSSE, 3000);
|
|
3243
|
-
};
|
|
3244
|
-
}
|
|
3886
|
+
es.onerror = () => {
|
|
3887
|
+
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
3888
|
+
es.close();
|
|
3889
|
+
setTimeout(connectSSE, 3000);
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3245
3892
|
|
|
3246
|
-
init().then(() => {
|
|
3247
|
-
restoreSessions();
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3893
|
+
init().then(async () => {
|
|
3894
|
+
restoreSessions();
|
|
3895
|
+
await loadAgentState();
|
|
3896
|
+
connectSSE();
|
|
3897
|
+
applyTerminalFiltersFromUi();
|
|
3898
|
+
// Sync content padding with terminal panel open state automatically
|
|
3899
|
+
new MutationObserver(() => {
|
|
3900
|
+
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
3901
|
+
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
3902
|
+
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
3254
3903
|
});
|
|
3255
3904
|
</script>
|
|
3256
3905
|
</body>
|