lance-context 1.36.2 → 1.37.1

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.
@@ -220,26 +220,17 @@ export function getDashboardHTML() {
220
220
 
221
221
  .grid {
222
222
  display: grid;
223
- grid-template-columns: repeat(3, 1fr);
223
+ grid-template-columns: repeat(2, 1fr);
224
224
  gap: 16px;
225
225
  }
226
226
 
227
- /* Tablet: 2 cards per row */
228
- @media (max-width: 1024px) {
229
- .grid {
230
- grid-template-columns: repeat(2, 1fr);
231
- }
232
- .card.double-width {
233
- grid-column: span 2;
234
- }
235
- }
236
-
237
227
  /* Mobile: 1 card per row */
238
228
  @media (max-width: 768px) {
239
229
  .grid {
240
230
  grid-template-columns: 1fr;
241
231
  }
242
- .card.double-width {
232
+ .card.double-width,
233
+ .card.half-width {
243
234
  grid-column: span 1;
244
235
  }
245
236
  }
@@ -563,6 +554,29 @@ export function getDashboardHTML() {
563
554
  grid-column: span 2;
564
555
  }
565
556
 
557
+ /* Half-width: takes 1 column (50% on 2-col grid) */
558
+ .card.half-width {
559
+ grid-column: span 1;
560
+ }
561
+
562
+ /* Compact card style */
563
+ .card.compact {
564
+ padding: 16px;
565
+ }
566
+
567
+ .card.compact .stat {
568
+ margin-bottom: 8px;
569
+ }
570
+
571
+ .card.compact .stat-value {
572
+ font-size: 20px;
573
+ }
574
+
575
+ .card.compact .reindex-actions {
576
+ margin-top: 8px;
577
+ padding-top: 8px;
578
+ }
579
+
566
580
  .empty-state {
567
581
  text-align: center;
568
582
  padding: 40px 20px;
@@ -783,16 +797,28 @@ export function getDashboardHTML() {
783
797
 
784
798
  /* Beads Section Styles */
785
799
  .beads-section {
786
- margin-top: 32px;
787
- padding-top: 24px;
788
- border-top: 1px solid var(--border-color);
800
+ /* Now a standalone tab, no border needed */
789
801
  }
790
802
 
791
803
  .beads-header {
792
804
  display: flex;
793
805
  align-items: center;
806
+ justify-content: space-between;
794
807
  gap: 12px;
795
808
  margin-bottom: 16px;
809
+ flex-wrap: wrap;
810
+ }
811
+
812
+ .beads-header-left {
813
+ display: flex;
814
+ align-items: center;
815
+ gap: 12px;
816
+ }
817
+
818
+ .beads-header-right {
819
+ display: flex;
820
+ align-items: center;
821
+ gap: 24px;
796
822
  }
797
823
 
798
824
  .beads-logo {
@@ -816,7 +842,6 @@ export function getDashboardHTML() {
816
842
  .beads-stats {
817
843
  display: flex;
818
844
  gap: 24px;
819
- margin-bottom: 16px;
820
845
  }
821
846
 
822
847
  .beads-stat {
@@ -909,7 +934,6 @@ export function getDashboardHTML() {
909
934
  gap: 8px;
910
935
  font-size: 12px;
911
936
  color: var(--text-secondary);
912
- margin-top: 12px;
913
937
  }
914
938
 
915
939
  .beads-empty {
@@ -967,6 +991,194 @@ export function getDashboardHTML() {
967
991
  font-style: italic;
968
992
  color: var(--text-muted);
969
993
  }
994
+
995
+ /* Tab navigation */
996
+ .tab-container {
997
+ margin-bottom: 24px;
998
+ }
999
+
1000
+ .tab-nav {
1001
+ display: flex;
1002
+ gap: 4px;
1003
+ border-bottom: 1px solid var(--border-color);
1004
+ padding-bottom: 0;
1005
+ }
1006
+
1007
+ .tab-btn {
1008
+ padding: 10px 20px;
1009
+ font-size: 14px;
1010
+ font-weight: 500;
1011
+ color: var(--text-secondary);
1012
+ background: transparent;
1013
+ border: none;
1014
+ border-bottom: 2px solid transparent;
1015
+ cursor: pointer;
1016
+ transition: all 0.2s ease;
1017
+ margin-bottom: -1px;
1018
+ }
1019
+
1020
+ .tab-btn:hover {
1021
+ color: var(--text-primary);
1022
+ background-color: var(--bg-tertiary);
1023
+ }
1024
+
1025
+ .tab-btn.active {
1026
+ color: var(--accent-blue);
1027
+ border-bottom-color: var(--accent-blue);
1028
+ }
1029
+
1030
+ .tab-content {
1031
+ display: none;
1032
+ }
1033
+
1034
+ .tab-content.active {
1035
+ display: block;
1036
+ }
1037
+
1038
+ /* Slider styles */
1039
+ .form-slider {
1040
+ width: 100%;
1041
+ height: 8px;
1042
+ border-radius: 4px;
1043
+ background: var(--bg-tertiary);
1044
+ outline: none;
1045
+ -webkit-appearance: none;
1046
+ appearance: none;
1047
+ cursor: pointer;
1048
+ }
1049
+
1050
+ .form-slider::-webkit-slider-thumb {
1051
+ -webkit-appearance: none;
1052
+ appearance: none;
1053
+ width: 20px;
1054
+ height: 20px;
1055
+ border-radius: 50%;
1056
+ background: var(--accent-blue);
1057
+ cursor: pointer;
1058
+ border: 2px solid var(--bg-primary);
1059
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
1060
+ }
1061
+
1062
+ .form-slider::-moz-range-thumb {
1063
+ width: 20px;
1064
+ height: 20px;
1065
+ border-radius: 50%;
1066
+ background: var(--accent-blue);
1067
+ cursor: pointer;
1068
+ border: 2px solid var(--bg-primary);
1069
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
1070
+ }
1071
+
1072
+ .slider-labels {
1073
+ display: flex;
1074
+ justify-content: space-between;
1075
+ margin-top: 8px;
1076
+ font-size: 12px;
1077
+ color: var(--text-muted);
1078
+ }
1079
+
1080
+ .slider-value {
1081
+ text-align: center;
1082
+ font-size: 14px;
1083
+ font-weight: 500;
1084
+ color: var(--text-primary);
1085
+ margin-top: 8px;
1086
+ }
1087
+
1088
+ /* Chip styles */
1089
+ .chips-container {
1090
+ display: flex;
1091
+ flex-wrap: wrap;
1092
+ gap: 8px;
1093
+ margin-bottom: 12px;
1094
+ }
1095
+
1096
+ .chip {
1097
+ display: inline-flex;
1098
+ align-items: center;
1099
+ gap: 6px;
1100
+ padding: 6px 10px;
1101
+ font-size: 13px;
1102
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
1103
+ background-color: var(--bg-tertiary);
1104
+ border: 1px solid var(--border-color);
1105
+ border-radius: 16px;
1106
+ color: var(--text-secondary);
1107
+ }
1108
+
1109
+ .chip.exclude {
1110
+ color: var(--accent-red);
1111
+ border-color: rgba(248, 81, 73, 0.3);
1112
+ background-color: rgba(248, 81, 73, 0.1);
1113
+ }
1114
+
1115
+ .chip-remove {
1116
+ display: flex;
1117
+ align-items: center;
1118
+ justify-content: center;
1119
+ width: 16px;
1120
+ height: 16px;
1121
+ border-radius: 50%;
1122
+ background: transparent;
1123
+ border: none;
1124
+ color: var(--text-muted);
1125
+ cursor: pointer;
1126
+ padding: 0;
1127
+ font-size: 14px;
1128
+ line-height: 1;
1129
+ transition: all 0.2s ease;
1130
+ }
1131
+
1132
+ .chip-remove:hover {
1133
+ background-color: var(--accent-red);
1134
+ color: white;
1135
+ }
1136
+
1137
+ .chip-input-group {
1138
+ display: flex;
1139
+ gap: 8px;
1140
+ margin-top: 8px;
1141
+ }
1142
+
1143
+ .chip-input-group .form-input {
1144
+ flex: 1;
1145
+ }
1146
+
1147
+ .chip-input-group .btn {
1148
+ flex-shrink: 0;
1149
+ }
1150
+
1151
+ .chips-empty {
1152
+ color: var(--text-muted);
1153
+ font-size: 13px;
1154
+ font-style: italic;
1155
+ }
1156
+
1157
+ /* Patterns section layout */
1158
+ .patterns-section {
1159
+ display: flex;
1160
+ flex-direction: column;
1161
+ gap: 24px;
1162
+ }
1163
+
1164
+ .patterns-group {
1165
+ display: flex;
1166
+ flex-direction: column;
1167
+ gap: 8px;
1168
+ }
1169
+
1170
+ .patterns-group-header {
1171
+ display: flex;
1172
+ align-items: center;
1173
+ gap: 8px;
1174
+ margin-bottom: 4px;
1175
+ }
1176
+
1177
+ .patterns-group-title {
1178
+ font-size: 14px;
1179
+ font-weight: 600;
1180
+ color: var(--text-primary);
1181
+ }
970
1182
  </style>
971
1183
  </head>
972
1184
  <body>
@@ -997,6 +1209,18 @@ export function getDashboardHTML() {
997
1209
  </div>
998
1210
  </header>
999
1211
 
1212
+ <!-- Tab Navigation -->
1213
+ <div class="tab-container">
1214
+ <nav class="tab-nav">
1215
+ <button class="tab-btn active" data-tab="status">Status</button>
1216
+ <button class="tab-btn" data-tab="beads">Beads</button>
1217
+ <button class="tab-btn" data-tab="settings">Settings</button>
1218
+ </nav>
1219
+ </div>
1220
+
1221
+ <!-- Status Tab -->
1222
+ <div id="tab-status" class="tab-content active">
1223
+
1000
1224
  <!-- Backend Fallback Warning Banner -->
1001
1225
  <div class="warning-banner" id="fallbackBanner">
1002
1226
  <div class="warning-banner-header">
@@ -1014,8 +1238,29 @@ export function getDashboardHTML() {
1014
1238
  </div>
1015
1239
 
1016
1240
  <div class="grid">
1241
+ <!-- Command Usage Card -->
1242
+ <div class="card full-width">
1243
+ <div class="card-header">
1244
+ <span class="card-title">Command Usage</span>
1245
+ <span class="badge" id="sessionBadge">This Session</span>
1246
+ </div>
1247
+ <div id="usageChartContainer">
1248
+ <div class="usage-empty" id="usageEmpty">No commands executed yet</div>
1249
+ <div id="chartWrapper">
1250
+ <table class="charts-css column show-primary-axis data-spacing-5" id="usage-chart" style="display: none;">
1251
+ <tbody id="usageChartBody"></tbody>
1252
+ </table>
1253
+ </div>
1254
+ <ul class="charts-css legend legend-inline legend-square" id="chartLegend" style="display: none;"></ul>
1255
+ <div class="usage-total" id="usageTotal" style="display: none;">
1256
+ <span class="usage-total-label">Total Commands</span>
1257
+ <span class="usage-total-count" id="totalCount">0</span>
1258
+ </div>
1259
+ </div>
1260
+ </div>
1261
+
1017
1262
  <!-- Index Status Card -->
1018
- <div class="card">
1263
+ <div class="card half-width compact">
1019
1264
  <div class="card-header">
1020
1265
  <span class="card-title">Index Status</span>
1021
1266
  <span class="badge" id="indexBadge">Loading...</span>
@@ -1045,132 +1290,8 @@ export function getDashboardHTML() {
1045
1290
  </div>
1046
1291
  </div>
1047
1292
 
1048
- <!-- Embedding Backend Card -->
1049
- <div class="card">
1050
- <div class="card-header">
1051
- <span class="card-title">Embedding Backend</span>
1052
- <span class="badge" id="embeddingStatus">-</span>
1053
- </div>
1054
- <div class="stat">
1055
- <div class="stat-label">Current Backend</div>
1056
- <div class="stat-value small" id="embeddingBackend">-</div>
1057
- </div>
1058
- <div class="stat">
1059
- <div class="stat-label">Index Path</div>
1060
- <div class="stat-value small" id="indexPath">-</div>
1061
- </div>
1062
- <div class="settings-form" id="embeddingSettingsForm">
1063
- <div class="form-group">
1064
- <label for="backendSelect">Select Backend</label>
1065
- <select id="backendSelect" class="form-select">
1066
- <option value="ollama">Ollama (local)</option>
1067
- <option value="gemini" selected>Google Gemini (free - requires API key)</option>
1068
- </select>
1069
- </div>
1070
- <div class="form-group" id="ollamaSettingsGroup">
1071
- <label for="concurrencySelect">Ollama Concurrency</label>
1072
- <select id="concurrencySelect" class="form-select">
1073
- <option value="1" selected>1 (default)</option>
1074
- <option value="2">2</option>
1075
- <option value="3">3</option>
1076
- <option value="5">5</option>
1077
- <option value="10">10</option>
1078
- <option value="25">25</option>
1079
- <option value="50">50</option>
1080
- <option value="100">100</option>
1081
- <option value="250">250</option>
1082
- <option value="500">500</option>
1083
- <option value="1000">1000</option>
1084
- </select>
1085
- </div>
1086
- <div class="form-group" id="batchSizeGroup">
1087
- <label for="batchSizeSelect">Batch Size</label>
1088
- <select id="batchSizeSelect" class="form-select">
1089
- <option value="32">32</option>
1090
- <option value="64">64</option>
1091
- <option value="128">128</option>
1092
- <option value="256" selected>256 (default)</option>
1093
- <option value="512">512</option>
1094
- <option value="1024">1024</option>
1095
- <option value="2048">2048</option>
1096
- <option value="4096">4096</option>
1097
- <option value="8192">8192</option>
1098
- <option value="16384">16384</option>
1099
- </select>
1100
- </div>
1101
- <div class="form-group" id="apiKeyGroup" style="display: none;">
1102
- <label for="apiKeyInput" id="apiKeyLabel">API Key</label>
1103
- <input type="password" id="apiKeyInput" class="form-input" placeholder="" />
1104
- <div class="form-hint" id="apiKeyHint"></div>
1105
- </div>
1106
- <div class="form-actions">
1107
- <button type="button" id="saveEmbeddingBtn" class="btn btn-primary">Save Settings</button>
1108
- <span id="saveStatus" class="save-status"></span>
1109
- </div>
1110
- </div>
1111
- </div>
1112
-
1113
- <!-- Dashboard Settings Card -->
1114
- <div class="card">
1115
- <div class="card-header">
1116
- <span class="card-title">Dashboard Settings</span>
1117
- <span class="badge" id="dashboardBadge">Enabled</span>
1118
- </div>
1119
- <div class="stat">
1120
- <div class="stat-label">Auto-Start on MCP Launch</div>
1121
- <div class="stat-value small" id="dashboardEnabled">-</div>
1122
- </div>
1123
- <div class="settings-form" id="dashboardSettingsForm">
1124
- <div class="form-group">
1125
- <label for="dashboardEnabledSelect">Dashboard Auto-Start</label>
1126
- <select id="dashboardEnabledSelect" class="form-select">
1127
- <option value="true">Enabled (auto-start with MCP server)</option>
1128
- <option value="false">Disabled (manual start only)</option>
1129
- </select>
1130
- </div>
1131
- <div class="form-hint">
1132
- When disabled, use the <code>open_dashboard</code> MCP tool to start manually.
1133
- </div>
1134
- <div class="form-actions">
1135
- <button type="button" id="saveDashboardBtn" class="btn btn-primary" style="display: none;">Save Settings</button>
1136
- <span id="saveDashboardStatus" class="save-status"></span>
1137
- </div>
1138
- </div>
1139
- </div>
1140
-
1141
- <!-- Configuration Card -->
1142
- <div class="card">
1143
- <div class="card-header">
1144
- <span class="card-title">Configuration</span>
1145
- </div>
1146
- <div class="stat">
1147
- <div class="stat-label">Project Path</div>
1148
- <div class="stat-value small" id="projectPath">-</div>
1149
- </div>
1150
- <div class="stat">
1151
- <div class="stat-label">Chunk Size</div>
1152
- <div class="stat-value small" id="chunkSize">-</div>
1153
- </div>
1154
- <div class="stat">
1155
- <div class="stat-label">Search Weights</div>
1156
- <div class="stat-value small" id="searchWeights">-</div>
1157
- </div>
1158
- <div class="stat">
1159
- <div class="stat-label">Include Patterns</div>
1160
- <div class="patterns-list" id="includePatterns">
1161
- <span class="pattern-tag">Loading...</span>
1162
- </div>
1163
- </div>
1164
- <div class="stat">
1165
- <div class="stat-label">Exclude Patterns</div>
1166
- <div class="patterns-list" id="excludePatterns">
1167
- <span class="pattern-tag exclude">Loading...</span>
1168
- </div>
1169
- </div>
1170
- </div>
1171
-
1172
1293
  <!-- Token Savings Card -->
1173
- <div class="card">
1294
+ <div class="card half-width compact">
1174
1295
  <div class="card-header">
1175
1296
  <span class="card-title">Token Savings</span>
1176
1297
  <span class="badge" id="savingsBadge">This Session</span>
@@ -1191,103 +1312,244 @@ export function getDashboardHTML() {
1191
1312
  <div class="stat-label">Operations Tracked</div>
1192
1313
  <div class="stat-value small" id="operationCount">0</div>
1193
1314
  </div>
1194
- <div class="form-hint" style="margin-top: 10px;">
1195
- Semantic search returns only relevant code chunks instead of entire files, saving context tokens.
1196
- </div>
1197
1315
  </div>
1316
+ </div>
1198
1317
 
1199
- <!-- Command Usage Card -->
1200
- <div class="card double-width">
1201
- <div class="card-header">
1202
- <span class="card-title">Command Usage</span>
1203
- <span class="badge" id="sessionBadge">This Session</span>
1318
+ <!-- Server Log Section -->
1319
+ <div class="log-section" id="logSection">
1320
+ <div class="log-header" id="logHeader">
1321
+ <svg class="log-toggle" id="logToggle" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
1322
+ <path d="M6 4l4 4-4 4"/>
1323
+ </svg>
1324
+ <span class="log-title">Server Logs</span>
1325
+ <span class="badge" id="logCount">0</span>
1326
+ <div class="log-actions">
1327
+ <button type="button" id="clearLogsBtn">Clear</button>
1204
1328
  </div>
1205
- <div id="usageChartContainer">
1206
- <div class="usage-empty" id="usageEmpty">No commands executed yet</div>
1207
- <div id="chartWrapper">
1208
- <table class="charts-css column show-primary-axis data-spacing-5" id="usage-chart" style="display: none;">
1209
- <tbody id="usageChartBody"></tbody>
1210
- </table>
1329
+ </div>
1330
+ <div class="log-container" id="logContainer">
1331
+ <div class="log-empty" id="logEmpty">No logs yet. Logs will appear when indexing or other server operations occur.</div>
1332
+ </div>
1333
+ </div>
1334
+
1335
+ </div><!-- End Status Tab -->
1336
+
1337
+ <!-- Beads Tab -->
1338
+ <div id="tab-beads" class="tab-content">
1339
+ <div class="beads-section" id="beadsSection">
1340
+ <div class="beads-header">
1341
+ <div class="beads-header-left">
1342
+ <svg class="beads-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1343
+ <circle cx="12" cy="5" r="3"/>
1344
+ <circle cx="12" cy="12" r="3"/>
1345
+ <circle cx="12" cy="19" r="3"/>
1346
+ <line x1="12" y1="8" x2="12" y2="9"/>
1347
+ <line x1="12" y1="15" x2="12" y2="16"/>
1348
+ </svg>
1349
+ <span class="beads-title">Beads Issue Tracker</span>
1350
+ <span class="badge success" id="beadsBadge" style="display: none;">Active</span>
1211
1351
  </div>
1212
- <ul class="charts-css legend legend-inline legend-square" id="chartLegend" style="display: none;"></ul>
1213
- <div class="usage-total" id="usageTotal" style="display: none;">
1214
- <span class="usage-total-label">Total Commands</span>
1215
- <span class="usage-total-count" id="totalCount">0</span>
1352
+ <div class="beads-header-right" id="beadsHeaderStats" style="display: none;">
1353
+ <div class="beads-daemon-status" id="beadsDaemonStatus">
1354
+ <div class="status-dot" id="beadsDaemonDot"></div>
1355
+ <span id="beadsDaemonText">Daemon status unknown</span>
1356
+ </div>
1357
+ <div class="beads-stats">
1358
+ <div class="beads-stat">
1359
+ <span class="beads-stat-value" id="beadsReadyCount">0</span>
1360
+ <span class="beads-stat-label">Ready</span>
1361
+ </div>
1362
+ <div class="beads-stat">
1363
+ <span class="beads-stat-value" id="beadsOpenCount">0</span>
1364
+ <span class="beads-stat-label">Open</span>
1365
+ </div>
1366
+ <div class="beads-stat">
1367
+ <span class="beads-stat-value" id="beadsTotalCount">0</span>
1368
+ <span class="beads-stat-label">Total</span>
1369
+ </div>
1370
+ </div>
1371
+ </div>
1372
+ </div>
1373
+ <div id="beadsUnavailable" class="beads-unavailable">
1374
+ <p>Beads is not configured for this project.</p>
1375
+ <p style="margin-top: 8px; font-size: 13px;">Visit <a href="https://github.com/steveyegge/beads" target="_blank" style="color: var(--accent-blue);">github.com/steveyegge/beads</a> to learn more.</p>
1376
+ </div>
1377
+ <div id="beadsContent" style="display: none;">
1378
+ <div class="card">
1379
+ <div class="card-header">
1380
+ <span class="card-title">Ready Tasks</span>
1381
+ <span class="badge" id="readyTasksBadge">0 tasks</span>
1382
+ </div>
1383
+ <div class="beads-issues" id="beadsIssuesList">
1384
+ <div class="beads-empty">No ready tasks</div>
1385
+ </div>
1216
1386
  </div>
1217
1387
  </div>
1218
1388
  </div>
1219
- </div>
1389
+ </div><!-- End Beads Tab -->
1220
1390
 
1221
- <!-- Beads Section -->
1222
- <div class="beads-section" id="beadsSection" style="display: none;">
1223
- <div class="beads-header">
1224
- <svg class="beads-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1225
- <circle cx="12" cy="5" r="3"/>
1226
- <circle cx="12" cy="12" r="3"/>
1227
- <circle cx="12" cy="19" r="3"/>
1228
- <line x1="12" y1="8" x2="12" y2="9"/>
1229
- <line x1="12" y1="15" x2="12" y2="16"/>
1230
- </svg>
1231
- <span class="beads-title">Beads Issue Tracker</span>
1232
- </div>
1391
+ <!-- Settings Tab -->
1392
+ <div id="tab-settings" class="tab-content">
1233
1393
  <div class="grid">
1394
+ <!-- Embedding Backend Card -->
1234
1395
  <div class="card">
1235
1396
  <div class="card-header">
1236
- <span class="card-title">Status</span>
1237
- <span class="badge success" id="beadsBadge">Active</span>
1397
+ <span class="card-title">Embedding Backend</span>
1398
+ <span class="badge" id="embeddingStatus">-</span>
1399
+ </div>
1400
+ <div class="stat">
1401
+ <div class="stat-label">Current Backend</div>
1402
+ <div class="stat-value small" id="embeddingBackend">-</div>
1403
+ </div>
1404
+ <div class="stat">
1405
+ <div class="stat-label">Index Path</div>
1406
+ <div class="stat-value small" id="indexPath">-</div>
1238
1407
  </div>
1239
- <div class="beads-stats">
1240
- <div class="beads-stat">
1241
- <span class="beads-stat-value" id="beadsReadyCount">0</span>
1242
- <span class="beads-stat-label">Ready</span>
1408
+ <div class="settings-form" id="embeddingSettingsForm">
1409
+ <div class="form-group">
1410
+ <label for="backendSelect">Select Backend</label>
1411
+ <select id="backendSelect" class="form-select">
1412
+ <option value="ollama">Ollama (local)</option>
1413
+ <option value="gemini" selected>Google Gemini (free - requires API key)</option>
1414
+ </select>
1243
1415
  </div>
1244
- <div class="beads-stat">
1245
- <span class="beads-stat-value" id="beadsOpenCount">0</span>
1246
- <span class="beads-stat-label">Open</span>
1416
+ <div class="form-group" id="ollamaSettingsGroup">
1417
+ <label for="concurrencySelect">Ollama Concurrency</label>
1418
+ <select id="concurrencySelect" class="form-select">
1419
+ <option value="1" selected>1 (default)</option>
1420
+ <option value="2">2</option>
1421
+ <option value="3">3</option>
1422
+ <option value="5">5</option>
1423
+ <option value="10">10</option>
1424
+ <option value="25">25</option>
1425
+ <option value="50">50</option>
1426
+ <option value="100">100</option>
1427
+ <option value="250">250</option>
1428
+ <option value="500">500</option>
1429
+ <option value="1000">1000</option>
1430
+ </select>
1247
1431
  </div>
1248
- <div class="beads-stat">
1249
- <span class="beads-stat-value" id="beadsTotalCount">0</span>
1250
- <span class="beads-stat-label">Total</span>
1432
+ <div class="form-group" id="batchSizeGroup">
1433
+ <label for="batchSizeSelect">Batch Size</label>
1434
+ <select id="batchSizeSelect" class="form-select">
1435
+ <option value="32">32</option>
1436
+ <option value="64">64</option>
1437
+ <option value="128">128</option>
1438
+ <option value="256" selected>256 (default)</option>
1439
+ <option value="512">512</option>
1440
+ <option value="1024">1024</option>
1441
+ <option value="2048">2048</option>
1442
+ <option value="4096">4096</option>
1443
+ <option value="8192">8192</option>
1444
+ <option value="16384">16384</option>
1445
+ </select>
1446
+ </div>
1447
+ <div class="form-group" id="apiKeyGroup" style="display: none;">
1448
+ <label for="apiKeyInput" id="apiKeyLabel">API Key</label>
1449
+ <input type="password" id="apiKeyInput" class="form-input" placeholder="" />
1450
+ <div class="form-hint" id="apiKeyHint"></div>
1451
+ </div>
1452
+ <div class="form-actions">
1453
+ <button type="button" id="saveEmbeddingBtn" class="btn btn-primary">Save Settings</button>
1454
+ <span id="saveStatus" class="save-status"></span>
1251
1455
  </div>
1252
1456
  </div>
1253
- <div class="beads-daemon-status" id="beadsDaemonStatus">
1254
- <div class="status-dot" id="beadsDaemonDot"></div>
1255
- <span id="beadsDaemonText">Daemon status unknown</span>
1457
+ </div>
1458
+
1459
+ <!-- Dashboard Settings Card -->
1460
+ <div class="card">
1461
+ <div class="card-header">
1462
+ <span class="card-title">Dashboard Settings</span>
1463
+ <span class="badge" id="dashboardBadge">Enabled</span>
1464
+ </div>
1465
+ <div class="stat">
1466
+ <div class="stat-label">Auto-Start on MCP Launch</div>
1467
+ <div class="stat-value small" id="dashboardEnabled">-</div>
1256
1468
  </div>
1257
- <div class="stat" style="margin-top: 12px;" id="beadsSyncBranchStat">
1258
- <div class="stat-label">Sync Branch</div>
1259
- <div class="stat-value small" id="beadsSyncBranch">-</div>
1469
+ <div class="settings-form" id="dashboardSettingsForm">
1470
+ <div class="form-group">
1471
+ <label for="dashboardEnabledSelect">Dashboard Auto-Start</label>
1472
+ <select id="dashboardEnabledSelect" class="form-select">
1473
+ <option value="true">Enabled (auto-start with MCP server)</option>
1474
+ <option value="false">Disabled (manual start only)</option>
1475
+ </select>
1476
+ </div>
1477
+ <div class="form-hint">
1478
+ When disabled, use the <code>open_dashboard</code> MCP tool to start manually.
1479
+ </div>
1480
+ <div class="form-actions">
1481
+ <button type="button" id="saveDashboardBtn" class="btn btn-primary" style="display: none;">Save Settings</button>
1482
+ <span id="saveDashboardStatus" class="save-status"></span>
1483
+ </div>
1260
1484
  </div>
1261
1485
  </div>
1262
1486
 
1263
- <div class="card" style="grid-column: span 2;">
1487
+ <!-- Search Weights Card -->
1488
+ <div class="card">
1264
1489
  <div class="card-header">
1265
- <span class="card-title">Ready Tasks</span>
1266
- <span class="badge" id="readyTasksBadge">0 tasks</span>
1490
+ <span class="card-title">Search Weights</span>
1491
+ </div>
1492
+ <div class="stat">
1493
+ <div class="stat-label">Semantic vs Keyword Balance</div>
1494
+ <div class="slider-value" id="weightsDisplay">Semantic: 70%, Keyword: 30%</div>
1495
+ </div>
1496
+ <div class="form-group" style="margin-top: 16px;">
1497
+ <input type="range" id="weightsSlider" class="form-slider" min="0" max="100" value="70">
1498
+ <div class="slider-labels">
1499
+ <span>100% Keyword</span>
1500
+ <span>100% Semantic</span>
1501
+ </div>
1502
+ </div>
1503
+ <div class="form-hint" style="margin-top: 12px;">
1504
+ Semantic search finds conceptually similar code. Keyword search matches exact terms.
1267
1505
  </div>
1268
- <div class="beads-issues" id="beadsIssuesList">
1269
- <div class="beads-empty">No ready tasks</div>
1506
+ <div class="form-actions">
1507
+ <button type="button" id="saveWeightsBtn" class="btn btn-primary" style="display: none;">Save Weights</button>
1508
+ <span id="saveWeightsStatus" class="save-status"></span>
1270
1509
  </div>
1271
1510
  </div>
1272
- </div>
1273
- </div>
1274
1511
 
1275
- <!-- Server Log Section -->
1276
- <div class="log-section" id="logSection">
1277
- <div class="log-header" id="logHeader">
1278
- <svg class="log-toggle" id="logToggle" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
1279
- <path d="M6 4l4 4-4 4"/>
1280
- </svg>
1281
- <span class="log-title">Server Logs</span>
1282
- <span class="badge" id="logCount">0</span>
1283
- <div class="log-actions">
1284
- <button type="button" id="clearLogsBtn">Clear</button>
1512
+ <!-- File Patterns Card -->
1513
+ <div class="card">
1514
+ <div class="card-header">
1515
+ <span class="card-title">File Patterns</span>
1516
+ </div>
1517
+ <div class="patterns-section">
1518
+ <div class="patterns-group">
1519
+ <div class="patterns-group-header">
1520
+ <span class="patterns-group-title">Include</span>
1521
+ <span class="badge" id="includePatternsCount">0</span>
1522
+ </div>
1523
+ <div class="stat-label">Files matching these patterns will be indexed</div>
1524
+ <div class="chips-container" id="includePatternsChips">
1525
+ <span class="chips-empty">Loading...</span>
1526
+ </div>
1527
+ <div class="chip-input-group">
1528
+ <input type="text" id="includePatternInput" class="form-input" placeholder="e.g., **/*.ts">
1529
+ <button type="button" id="addIncludePatternBtn" class="btn btn-secondary">Add</button>
1530
+ </div>
1531
+ <span id="includePatternStatus" class="save-status"></span>
1532
+ </div>
1533
+ <div class="patterns-group">
1534
+ <div class="patterns-group-header">
1535
+ <span class="patterns-group-title">Exclude</span>
1536
+ <span class="badge" id="excludePatternsCount">0</span>
1537
+ </div>
1538
+ <div class="stat-label">Files matching these patterns will be skipped</div>
1539
+ <div class="chips-container" id="excludePatternsChips">
1540
+ <span class="chips-empty">Loading...</span>
1541
+ </div>
1542
+ <div class="chip-input-group">
1543
+ <input type="text" id="excludePatternInput" class="form-input" placeholder="e.g., **/node_modules/**">
1544
+ <button type="button" id="addExcludePatternBtn" class="btn btn-secondary">Add</button>
1545
+ </div>
1546
+ <span id="excludePatternStatus" class="save-status"></span>
1547
+ </div>
1548
+ </div>
1285
1549
  </div>
1286
1550
  </div>
1287
- <div class="log-container" id="logContainer">
1288
- <div class="log-empty" id="logEmpty">No logs yet. Logs will appear when indexing or other server operations occur.</div>
1289
- </div>
1290
- </div>
1551
+ </div><!-- End Settings Tab -->
1552
+
1291
1553
  </div>
1292
1554
 
1293
1555
  <script>
@@ -1313,6 +1575,33 @@ export function getDashboardHTML() {
1313
1575
  // Theme toggle button
1314
1576
  document.getElementById('themeToggle').addEventListener('click', toggleTheme);
1315
1577
 
1578
+ // Tab management
1579
+ function getStoredTab() {
1580
+ return localStorage.getItem('lance-context-tab') || 'status';
1581
+ }
1582
+
1583
+ function setActiveTab(tabId) {
1584
+ // Update buttons
1585
+ document.querySelectorAll('.tab-btn').forEach(btn => {
1586
+ btn.classList.toggle('active', btn.getAttribute('data-tab') === tabId);
1587
+ });
1588
+ // Update content
1589
+ document.querySelectorAll('.tab-content').forEach(content => {
1590
+ content.classList.toggle('active', content.id === 'tab-' + tabId);
1591
+ });
1592
+ localStorage.setItem('lance-context-tab', tabId);
1593
+ }
1594
+
1595
+ // Initialize tabs
1596
+ setActiveTab(getStoredTab());
1597
+
1598
+ // Tab click handlers
1599
+ document.querySelectorAll('.tab-btn').forEach(btn => {
1600
+ btn.addEventListener('click', () => {
1601
+ setActiveTab(btn.getAttribute('data-tab'));
1602
+ });
1603
+ });
1604
+
1316
1605
  // State
1317
1606
  let isConnected = false;
1318
1607
  let eventSource = null;
@@ -1331,11 +1620,6 @@ export function getDashboardHTML() {
1331
1620
  const indexPath = document.getElementById('indexPath');
1332
1621
  const fallbackBanner = document.getElementById('fallbackBanner');
1333
1622
  const fallbackContent = document.getElementById('fallbackContent');
1334
- const projectPath = document.getElementById('projectPath');
1335
- const chunkSize = document.getElementById('chunkSize');
1336
- const searchWeights = document.getElementById('searchWeights');
1337
- const includePatterns = document.getElementById('includePatterns');
1338
- const excludePatterns = document.getElementById('excludePatterns');
1339
1623
  const progressContainer = document.getElementById('progressContainer');
1340
1624
  const progressFill = document.getElementById('progressFill');
1341
1625
  const progressText = document.getElementById('progressText');
@@ -1685,6 +1969,228 @@ export function getDashboardHTML() {
1685
1969
  // Load dashboard settings on page load
1686
1970
  loadDashboardSettings();
1687
1971
 
1972
+ // ===== Search Weights Settings =====
1973
+ const weightsSlider = document.getElementById('weightsSlider');
1974
+ const weightsDisplay = document.getElementById('weightsDisplay');
1975
+ const saveWeightsBtn = document.getElementById('saveWeightsBtn');
1976
+ const saveWeightsStatus = document.getElementById('saveWeightsStatus');
1977
+ let savedSemanticWeight = 70;
1978
+
1979
+ function updateWeightsDisplay() {
1980
+ const semantic = parseInt(weightsSlider.value, 10);
1981
+ const keyword = 100 - semantic;
1982
+ weightsDisplay.textContent = 'Semantic: ' + semantic + '%, Keyword: ' + keyword + '%';
1983
+ }
1984
+
1985
+ function hasWeightsChanged() {
1986
+ return parseInt(weightsSlider.value, 10) !== savedSemanticWeight;
1987
+ }
1988
+
1989
+ function updateWeightsSaveButtonVisibility() {
1990
+ saveWeightsBtn.style.display = hasWeightsChanged() ? 'inline-block' : 'none';
1991
+ saveWeightsStatus.textContent = '';
1992
+ }
1993
+
1994
+ weightsSlider.addEventListener('input', function() {
1995
+ updateWeightsDisplay();
1996
+ updateWeightsSaveButtonVisibility();
1997
+ });
1998
+
1999
+ async function loadSearchWeights() {
2000
+ try {
2001
+ const response = await fetch('/api/search-weights');
2002
+ if (response.ok) {
2003
+ const weights = await response.json();
2004
+ const semantic = Math.round(weights.semanticWeight * 100);
2005
+ savedSemanticWeight = semantic;
2006
+ weightsSlider.value = semantic;
2007
+ updateWeightsDisplay();
2008
+ updateWeightsSaveButtonVisibility();
2009
+ }
2010
+ } catch (error) {
2011
+ console.error('Failed to load search weights:', error);
2012
+ }
2013
+ }
2014
+
2015
+ saveWeightsBtn.addEventListener('click', async function() {
2016
+ const semantic = parseInt(weightsSlider.value, 10) / 100;
2017
+ const keyword = 1 - semantic;
2018
+
2019
+ saveWeightsBtn.disabled = true;
2020
+ saveWeightsStatus.textContent = 'Saving...';
2021
+ saveWeightsStatus.className = 'save-status';
2022
+
2023
+ try {
2024
+ const response = await fetch('/api/search-weights', {
2025
+ method: 'PUT',
2026
+ headers: { 'Content-Type': 'application/json' },
2027
+ body: JSON.stringify({ semanticWeight: semantic, keywordWeight: keyword })
2028
+ });
2029
+
2030
+ const result = await response.json();
2031
+
2032
+ if (response.ok) {
2033
+ savedSemanticWeight = parseInt(weightsSlider.value, 10);
2034
+ saveWeightsStatus.textContent = 'Saved!';
2035
+ saveWeightsStatus.className = 'save-status success';
2036
+ updateWeightsSaveButtonVisibility();
2037
+ fetchData(); // Refresh config display
2038
+ } else {
2039
+ saveWeightsStatus.textContent = result.error || 'Failed to save';
2040
+ saveWeightsStatus.className = 'save-status error';
2041
+ }
2042
+ } catch (error) {
2043
+ saveWeightsStatus.textContent = 'Network error';
2044
+ saveWeightsStatus.className = 'save-status error';
2045
+ } finally {
2046
+ saveWeightsBtn.disabled = false;
2047
+ }
2048
+ });
2049
+
2050
+ loadSearchWeights();
2051
+
2052
+ // ===== Include/Exclude Patterns =====
2053
+ const includePatternsChips = document.getElementById('includePatternsChips');
2054
+ const excludePatternsChips = document.getElementById('excludePatternsChips');
2055
+ const includePatternsCount = document.getElementById('includePatternsCount');
2056
+ const excludePatternsCount = document.getElementById('excludePatternsCount');
2057
+ const includePatternInput = document.getElementById('includePatternInput');
2058
+ const excludePatternInput = document.getElementById('excludePatternInput');
2059
+ const addIncludePatternBtn = document.getElementById('addIncludePatternBtn');
2060
+ const addExcludePatternBtn = document.getElementById('addExcludePatternBtn');
2061
+ const includePatternStatus = document.getElementById('includePatternStatus');
2062
+ const excludePatternStatus = document.getElementById('excludePatternStatus');
2063
+
2064
+ function renderPatternChips(container, patterns, type, countBadge) {
2065
+ // Clear container safely
2066
+ while (container.firstChild) {
2067
+ container.removeChild(container.firstChild);
2068
+ }
2069
+
2070
+ if (!patterns || patterns.length === 0) {
2071
+ const empty = document.createElement('span');
2072
+ empty.className = 'chips-empty';
2073
+ empty.textContent = 'No patterns configured';
2074
+ container.appendChild(empty);
2075
+ countBadge.textContent = '0';
2076
+ return;
2077
+ }
2078
+
2079
+ countBadge.textContent = String(patterns.length);
2080
+
2081
+ patterns.forEach(function(pattern) {
2082
+ const chip = document.createElement('span');
2083
+ chip.className = type === 'exclude' ? 'chip exclude' : 'chip';
2084
+
2085
+ const patternText = document.createTextNode(pattern);
2086
+ chip.appendChild(patternText);
2087
+
2088
+ const removeBtn = document.createElement('button');
2089
+ removeBtn.type = 'button';
2090
+ removeBtn.className = 'chip-remove';
2091
+ removeBtn.title = 'Remove pattern';
2092
+ removeBtn.textContent = '\\u00d7';
2093
+
2094
+ removeBtn.addEventListener('click', async function(e) {
2095
+ e.stopPropagation();
2096
+ await removePatternFromConfig(pattern, type);
2097
+ });
2098
+
2099
+ chip.appendChild(removeBtn);
2100
+ container.appendChild(chip);
2101
+ });
2102
+ }
2103
+
2104
+ async function removePatternFromConfig(pattern, type) {
2105
+ const statusEl = type === 'include' ? includePatternStatus : excludePatternStatus;
2106
+ statusEl.textContent = 'Removing...';
2107
+ statusEl.className = 'save-status';
2108
+
2109
+ try {
2110
+ const response = await fetch('/api/patterns', {
2111
+ method: 'DELETE',
2112
+ headers: { 'Content-Type': 'application/json' },
2113
+ body: JSON.stringify({ pattern, type })
2114
+ });
2115
+
2116
+ const result = await response.json();
2117
+
2118
+ if (response.ok) {
2119
+ statusEl.textContent = 'Removed!';
2120
+ statusEl.className = 'save-status success';
2121
+ setTimeout(function() { statusEl.textContent = ''; }, 2000);
2122
+ fetchData(); // Refresh patterns
2123
+ } else {
2124
+ statusEl.textContent = result.error || 'Failed to remove';
2125
+ statusEl.className = 'save-status error';
2126
+ }
2127
+ } catch (error) {
2128
+ statusEl.textContent = 'Network error';
2129
+ statusEl.className = 'save-status error';
2130
+ }
2131
+ }
2132
+
2133
+ async function addPatternToConfig(pattern, type) {
2134
+ const statusEl = type === 'include' ? includePatternStatus : excludePatternStatus;
2135
+ const inputEl = type === 'include' ? includePatternInput : excludePatternInput;
2136
+
2137
+ if (!pattern.trim()) {
2138
+ statusEl.textContent = 'Pattern is required';
2139
+ statusEl.className = 'save-status error';
2140
+ return;
2141
+ }
2142
+
2143
+ statusEl.textContent = 'Adding...';
2144
+ statusEl.className = 'save-status';
2145
+
2146
+ try {
2147
+ const response = await fetch('/api/patterns', {
2148
+ method: 'POST',
2149
+ headers: { 'Content-Type': 'application/json' },
2150
+ body: JSON.stringify({ pattern: pattern.trim(), type })
2151
+ });
2152
+
2153
+ const result = await response.json();
2154
+
2155
+ if (response.ok) {
2156
+ inputEl.value = '';
2157
+ statusEl.textContent = 'Added!';
2158
+ statusEl.className = 'save-status success';
2159
+ setTimeout(function() { statusEl.textContent = ''; }, 2000);
2160
+ fetchData(); // Refresh patterns
2161
+ } else {
2162
+ statusEl.textContent = result.error || 'Failed to add';
2163
+ statusEl.className = 'save-status error';
2164
+ }
2165
+ } catch (error) {
2166
+ statusEl.textContent = 'Network error';
2167
+ statusEl.className = 'save-status error';
2168
+ }
2169
+ }
2170
+
2171
+ addIncludePatternBtn.addEventListener('click', function() {
2172
+ addPatternToConfig(includePatternInput.value, 'include');
2173
+ });
2174
+
2175
+ addExcludePatternBtn.addEventListener('click', function() {
2176
+ addPatternToConfig(excludePatternInput.value, 'exclude');
2177
+ });
2178
+
2179
+ // Handle Enter key for pattern inputs
2180
+ includePatternInput.addEventListener('keydown', function(e) {
2181
+ if (e.key === 'Enter') {
2182
+ e.preventDefault();
2183
+ addPatternToConfig(includePatternInput.value, 'include');
2184
+ }
2185
+ });
2186
+
2187
+ excludePatternInput.addEventListener('keydown', function(e) {
2188
+ if (e.key === 'Enter') {
2189
+ e.preventDefault();
2190
+ addPatternToConfig(excludePatternInput.value, 'exclude');
2191
+ }
2192
+ });
2193
+
1688
2194
  // Format date
1689
2195
  function formatDate(isoString) {
1690
2196
  if (!isoString) return 'Never';
@@ -1750,8 +2256,6 @@ export function getDashboardHTML() {
1750
2256
 
1751
2257
  // Update config display
1752
2258
  function updateConfig(config) {
1753
- projectPath.textContent = config.projectPath || '-';
1754
-
1755
2259
  // Update project name in header
1756
2260
  if (config.projectName) {
1757
2261
  projectNameHeader.textContent = config.projectName;
@@ -1760,33 +2264,12 @@ export function getDashboardHTML() {
1760
2264
  projectNameHeader.textContent = config.projectPath.split('/').pop() || config.projectPath;
1761
2265
  }
1762
2266
 
1763
- if (config.chunking) {
1764
- chunkSize.textContent = config.chunking.maxLines + ' lines (overlap: ' + config.chunking.overlap + ')';
1765
- }
1766
-
1767
- if (config.search) {
1768
- searchWeights.textContent = 'Semantic: ' + (config.search.semanticWeight * 100) + '%, Keyword: ' + (config.search.keywordWeight * 100) + '%';
1769
- }
1770
-
1771
- // Update patterns
2267
+ // Update Settings tab pattern chips
1772
2268
  if (config.patterns) {
1773
- includePatterns.innerHTML = config.patterns
1774
- .slice(0, 10)
1775
- .map(p => '<span class="pattern-tag">' + escapeHtml(p) + '</span>')
1776
- .join('');
1777
- if (config.patterns.length > 10) {
1778
- includePatterns.innerHTML += '<span class="pattern-tag">+' + (config.patterns.length - 10) + ' more</span>';
1779
- }
2269
+ renderPatternChips(includePatternsChips, config.patterns, 'include', includePatternsCount);
1780
2270
  }
1781
-
1782
2271
  if (config.excludePatterns) {
1783
- excludePatterns.innerHTML = config.excludePatterns
1784
- .slice(0, 6)
1785
- .map(p => '<span class="pattern-tag exclude">' + escapeHtml(p) + '</span>')
1786
- .join('');
1787
- if (config.excludePatterns.length > 6) {
1788
- excludePatterns.innerHTML += '<span class="pattern-tag exclude">+' + (config.excludePatterns.length - 6) + ' more</span>';
1789
- }
2272
+ renderPatternChips(excludePatternsChips, config.excludePatterns, 'exclude', excludePatternsCount);
1790
2273
  }
1791
2274
  }
1792
2275
 
@@ -1945,17 +2428,27 @@ export function getDashboardHTML() {
1945
2428
  const beadsTotalCount = document.getElementById('beadsTotalCount');
1946
2429
  const beadsDaemonDot = document.getElementById('beadsDaemonDot');
1947
2430
  const beadsDaemonText = document.getElementById('beadsDaemonText');
1948
- const beadsSyncBranch = document.getElementById('beadsSyncBranch');
1949
2431
  const beadsIssuesList = document.getElementById('beadsIssuesList');
1950
2432
  const readyTasksBadge = document.getElementById('readyTasksBadge');
1951
2433
 
1952
2434
  function updateBeads(data) {
2435
+ const beadsUnavailable = document.getElementById('beadsUnavailable');
2436
+ const beadsContent = document.getElementById('beadsContent');
2437
+ const beadsHeaderStats = document.getElementById('beadsHeaderStats');
2438
+ const beadsBadgeEl = document.getElementById('beadsBadge');
2439
+
1953
2440
  if (!data.available) {
1954
- beadsSection.style.display = 'none';
2441
+ beadsUnavailable.style.display = 'block';
2442
+ beadsContent.style.display = 'none';
2443
+ beadsHeaderStats.style.display = 'none';
2444
+ beadsBadgeEl.style.display = 'none';
1955
2445
  return;
1956
2446
  }
1957
2447
 
1958
- beadsSection.style.display = 'block';
2448
+ beadsUnavailable.style.display = 'none';
2449
+ beadsContent.style.display = 'block';
2450
+ beadsHeaderStats.style.display = 'flex';
2451
+ beadsBadgeEl.style.display = 'inline-flex';
1959
2452
  beadsReadyCount.textContent = data.readyCount;
1960
2453
  beadsOpenCount.textContent = data.openCount;
1961
2454
  beadsTotalCount.textContent = data.issueCount;
@@ -1970,9 +2463,6 @@ export function getDashboardHTML() {
1970
2463
  beadsDaemonText.textContent = 'Daemon not running';
1971
2464
  }
1972
2465
 
1973
- // Sync branch
1974
- beadsSyncBranch.textContent = data.syncBranch || 'Not configured';
1975
-
1976
2466
  // Issues list
1977
2467
  if (data.issues && data.issues.length > 0) {
1978
2468
  let html = '';