beast-agent 2.12.0 → 2.14.0
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/package.json +1 -1
- package/src/agent/android.js +265 -0
- package/src/agent/engine.js +79 -0
- package/src/agent/headroom.js +279 -0
- package/src/agent/llm.js +20 -2
- package/src/main.js +209 -0
- package/src/preload.js +8 -0
- package/src/renderer/i18n.js +7 -3
- package/src/renderer/index.html +20 -0
- package/src/renderer/renderer.js +289 -83
- package/src/renderer/style.css +118 -0
package/src/renderer/renderer.js
CHANGED
|
@@ -43,6 +43,8 @@ const els = {
|
|
|
43
43
|
railBtn: $('#railBtn'),
|
|
44
44
|
railClear: $('#railClear'),
|
|
45
45
|
watchBtn: $('#watchBtn'),
|
|
46
|
+
watchPaneList: $('#watchPaneList'),
|
|
47
|
+
watchPaneOpen: $('#watchPaneOpen'),
|
|
46
48
|
cronBtn: $('#cronBtn'),
|
|
47
49
|
watchOverlay: $('#watchOverlay'),
|
|
48
50
|
cronOverlay: $('#cronOverlay'),
|
|
@@ -83,6 +85,10 @@ const els = {
|
|
|
83
85
|
bcAttach: $('#bcAttach'),
|
|
84
86
|
bcMic: $('#bcMic'),
|
|
85
87
|
bcChips: $('#bcChips'),
|
|
88
|
+
bcQrBtn: $('#bcQrBtn'),
|
|
89
|
+
bcQrCard: $('#bcQrCard'),
|
|
90
|
+
bcQrImg: $('#bcQrImg'),
|
|
91
|
+
bcQrUrl: $('#bcQrUrl'),
|
|
86
92
|
studioBtn: $('#studioBtn'),
|
|
87
93
|
sbBtn: $('#sbBtn'),
|
|
88
94
|
sandboxRow: $('#sandboxRow'),
|
|
@@ -951,8 +957,19 @@ let dragSid = null;
|
|
|
951
957
|
async function refreshSessions() {
|
|
952
958
|
await renderSessions(await beast.listSessions());
|
|
953
959
|
renderBotCards(); // bot kartlarındaki numara/sayı etiketleri de tazelensin
|
|
960
|
+
renderWatchersPane().catch(() => {}); // kenar izleyici listesi de tazelensin
|
|
954
961
|
}
|
|
955
962
|
|
|
963
|
+
/* İzleyici kenar pane canlı tazeleme: agent modunda 8 sn'de bir sessiz çekim;
|
|
964
|
+
başka moddaysa zamanlayıcı pas geçer (IPC maliyeti yok) */
|
|
965
|
+
setInterval(() => {
|
|
966
|
+
try {
|
|
967
|
+
if (document.body.classList.contains('ide-mode') || document.body.classList.contains('studio-mode') || document.body.classList.contains('sandbox-mode')) return;
|
|
968
|
+
if (document.hidden) return;
|
|
969
|
+
renderWatchersPane().catch(() => {});
|
|
970
|
+
} catch {}
|
|
971
|
+
}, 8000);
|
|
972
|
+
|
|
956
973
|
async function openSession(id) {
|
|
957
974
|
activeId = id;
|
|
958
975
|
streamEl = null;
|
|
@@ -1089,6 +1106,7 @@ function addFiles(files, mode) {
|
|
|
1089
1106
|
/* ---------------- settings ---------------- */
|
|
1090
1107
|
|
|
1091
1108
|
let setTab = 'lang'; /* ayarlar artık DİL sekmesiyle açılır */
|
|
1109
|
+
let hrPaneTimer = null; /* headroom-status → Kurulum sekmesi debounce tazeleme */
|
|
1092
1110
|
|
|
1093
1111
|
/* DİL sekmesi: arayüz dili seçimi (eski sağ-alt dil ikonunun yerine) */
|
|
1094
1112
|
function renderLangPane() {
|
|
@@ -1154,6 +1172,7 @@ async function renderActiveSettingsTab() {
|
|
|
1154
1172
|
case 'empati': await renderEmpatiPane(); break;
|
|
1155
1173
|
case 'cron': await openCron(); break;
|
|
1156
1174
|
case 'usage': await renderUsagePane(); break;
|
|
1175
|
+
case 'headroom': await renderHeadroomPane(); break;
|
|
1157
1176
|
case 'agents': await refreshAgentsPane(); break;
|
|
1158
1177
|
case 'logs': await renderLogPane(); break;
|
|
1159
1178
|
case 'dash': await renderDashboardPane(); break;
|
|
@@ -1237,7 +1256,7 @@ function switchTab(name) {
|
|
|
1237
1256
|
document.querySelectorAll('#setTabs .tab').forEach((b) =>
|
|
1238
1257
|
b.classList.toggle('active', b.dataset.tab === name)
|
|
1239
1258
|
);
|
|
1240
|
-
for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
|
|
1259
|
+
for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'usage', 'headroom', 'logs', 'dash', 'sec', 'update']) {
|
|
1241
1260
|
const el = $('#tab-' + p);
|
|
1242
1261
|
if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
|
|
1243
1262
|
}
|
|
@@ -1245,6 +1264,7 @@ function switchTab(name) {
|
|
|
1245
1264
|
if (name === 'cron') openCron();
|
|
1246
1265
|
if (name === 'usage') renderUsagePane();
|
|
1247
1266
|
if (name === 'install') renderInstallPane();
|
|
1267
|
+
if (name === 'headroom') renderHeadroomPane();
|
|
1248
1268
|
if (name === 'events') renderEventsPane();
|
|
1249
1269
|
if (name === 'empati') renderEmpatiPane();
|
|
1250
1270
|
if (name === 'logs') renderLogPane();
|
|
@@ -1272,7 +1292,6 @@ async function renderUsagePane() {
|
|
|
1272
1292
|
} catch {
|
|
1273
1293
|
rep = { today: { total: {}, models: [] }, month: { total: {}, models: [] } };
|
|
1274
1294
|
}
|
|
1275
|
-
const wwi = await beast.whereWasIGet().catch(() => ({ enabled: false }));
|
|
1276
1295
|
const t = rep.today.total || {};
|
|
1277
1296
|
const mo = rep.month.total || {};
|
|
1278
1297
|
const card = (label, v) =>
|
|
@@ -1307,10 +1326,7 @@ async function renderUsagePane() {
|
|
|
1307
1326
|
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
|
|
1308
1327
|
'<button id="bkNow" class="btn ghost">' + _t('us_backup_now') + '</button>' +
|
|
1309
1328
|
'<button id="bkRestore" class="btn ghost">' + _t('us_backup_restore') + '</button>' +
|
|
1310
|
-
'</div>'
|
|
1311
|
-
'<div class="divider"></div><h2>' + _t('us_where_h2') + '</h2>' +
|
|
1312
|
-
'<div class="sub">' + _t('us_where_sub') + '</div>' +
|
|
1313
|
-
`<label class="lock-row" style="margin-top:6px"><input type="checkbox" id="wwiOn" ${wwi.enabled ? 'checked' : ''}/><span>${_t('us_where_on')}</span></label>`;
|
|
1329
|
+
'</div>';
|
|
1314
1330
|
$('#usReset').addEventListener('click', async () => {
|
|
1315
1331
|
await beast.resetUsage();
|
|
1316
1332
|
renderUsagePane();
|
|
@@ -1330,15 +1346,6 @@ async function renderUsagePane() {
|
|
|
1330
1346
|
toast(_t('us_backup_err') + (r.error || '?'));
|
|
1331
1347
|
}
|
|
1332
1348
|
});
|
|
1333
|
-
$('#wwiOn').addEventListener('change', async (e) => {
|
|
1334
|
-
await beast.whereWasISet({ enabled: e.target.checked });
|
|
1335
|
-
toast(e.target.checked ? _t('us_where_on_toast') : _t('us_where_off_toast'));
|
|
1336
|
-
});
|
|
1337
|
-
/* LİMİT ayarları aynı sekmenin alt bölümünde (Maliyet · Limit birleşik) */
|
|
1338
|
-
const limBox = document.createElement('div');
|
|
1339
|
-
limBox.id = 'limBox';
|
|
1340
|
-
pane.appendChild(limBox);
|
|
1341
|
-
await renderLimitsPane();
|
|
1342
1349
|
}
|
|
1343
1350
|
|
|
1344
1351
|
async function refreshFalloutPane() {
|
|
@@ -2215,6 +2222,93 @@ function ttsFlushTail() {
|
|
|
2215
2222
|
/* ---------------- KURULUM SEKMESİ: bileşenler OTOMATİK kurulur ----------------
|
|
2216
2223
|
Eksik/kısmen inmiş bileşen sekme açılınca kendiliğinden indirilir; kullanıcı
|
|
2217
2224
|
hiçbir düğmeye basmaz. Durum canlı güncellenir (4 sn'de bir). */
|
|
2225
|
+
/* ---------- TOKEN SIKIŞTIRMA (Headroom) sekmesi ----------
|
|
2226
|
+
Opsiyonel yerel sıkıştırma proxy'si: açıkken LLM girdisi %30-70 küçülür.
|
|
2227
|
+
Aç/Kapat buradan; CLI kurulumu Kurulum sekmesinde görülür. */
|
|
2228
|
+
let hrStatsTimer = null;
|
|
2229
|
+
|
|
2230
|
+
async function renderHeadroomPane() {
|
|
2231
|
+
const pane = $('#tab-headroom');
|
|
2232
|
+
if (!pane) return;
|
|
2233
|
+
const st = await beast.headroomStatus().catch(() => null);
|
|
2234
|
+
|
|
2235
|
+
const fmtN = (n) => new Intl.NumberFormat('tr-TR').format(Math.round(Number(n) || 0));
|
|
2236
|
+
let stHtml = '';
|
|
2237
|
+
if (st) {
|
|
2238
|
+
const readyN = (st.proxies || []).filter((p) => p.ready).length;
|
|
2239
|
+
let stateTxt;
|
|
2240
|
+
if (st.installing) stateTxt = '⏳ arka planda kuruluyor — uv tool install headroom-ai[proxy] (birkaç dakika)';
|
|
2241
|
+
else if (st.enabled && readyN) stateTxt = '🟢 açık — proxy: ' + (st.proxies || []).filter((p) => p.ready).map((p) => '127.0.0.1:' + p.port).join(', ');
|
|
2242
|
+
else if (st.enabled) stateTxt = '⏳ proxy başlatılıyor… (ilk açılış ~30 sn)';
|
|
2243
|
+
else if (st.failed) stateTxt = '⛔ ' + (st.error || 'kurulum başarısız');
|
|
2244
|
+
else if (st.installed) stateTxt = '⚪ kapalı — kurulu, aşağıdaki düğmeyle etkinleştir';
|
|
2245
|
+
else stateTxt = '⚪ kapalı — açınca uv ile otomatik kurulur (birkaç dakika)';
|
|
2246
|
+
stHtml = '<div class="sub" style="margin-top:10px;font-size:12px;text-align:center">' + escapeHtml(stateTxt) + '</div>';
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
const stats = await beast.headroomStats().catch(() => null);
|
|
2250
|
+
let statsHtml = '';
|
|
2251
|
+
if (stats && stats.ok && (stats.input || stats.saved)) {
|
|
2252
|
+
statsHtml =
|
|
2253
|
+
'<div class="divider"></div>' +
|
|
2254
|
+
'<h2 style="text-align:center">Tasarruf (proxy açıkken)</h2>' +
|
|
2255
|
+
'<div style="display:flex;gap:24px;flex-wrap:wrap;margin-top:8px;justify-content:center">' +
|
|
2256
|
+
'<div><div style="font-size:20px;font-weight:800;color:var(--ok)">%' + Math.round(stats.savingsPercent || 0) + '</div><div class="sub">sıkıştırma oranı</div></div>' +
|
|
2257
|
+
'<div><div style="font-size:20px;font-weight:800">' + new Intl.NumberFormat('tr-TR').format(stats.proxyCompressionSaved || 0) + '</div><div class="sub">kaydedilen token</div></div>' +
|
|
2258
|
+
'<div><div style="font-size:20px;font-weight:800">' + new Intl.NumberFormat('tr-TR').format(stats.input || 0) + '</div><div class="sub">toplam girdi token</div></div>' +
|
|
2259
|
+
'</div>';
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
/* AÇ/KAPAT düğmesi: ortalanmış + SVG ikonlu (play/pause — uygulama ikon stili) */
|
|
2263
|
+
const svgPlay =
|
|
2264
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 3 20 12 6 21 6 3"/></svg>';
|
|
2265
|
+
const svgPause =
|
|
2266
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
|
|
2267
|
+
pane.innerHTML =
|
|
2268
|
+
'<h2 style="text-align:center">' + _t('tab_headroom') + '</h2>' +
|
|
2269
|
+
'<div class="sub" style="text-align:center;max-width:620px;margin:0 auto">Headroom yerel proxy\u2019si — araç çıktıları, loglar ve JSON yükleri modele gitmeden sıkıştırılır; orijinali yerelde saklanır. Token faturasını %30-70 düşürür. Yalnız OpenAI-uyumlu sağlayıcı trafiğini etkiler; kapalıyken hiçbir şey değişmez.</div>' +
|
|
2270
|
+
'<div style="display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:18px">' +
|
|
2271
|
+
'<button id="hrToggle" class="btn" style="width:auto;padding:11px 16px;display:inline-flex;align-items:center;gap:8px;justify-content:center" title=""></button>' +
|
|
2272
|
+
'<span id="hrToggleMsg" class="sub" style="margin:0;text-align:center;max-width:560px"></span>' +
|
|
2273
|
+
'</div>' +
|
|
2274
|
+
stHtml + statsHtml;
|
|
2275
|
+
|
|
2276
|
+
const btn = $('#hrToggle');
|
|
2277
|
+
if (btn) {
|
|
2278
|
+
const on = !!(st && st.enabled);
|
|
2279
|
+
/* yalnız SVG ikon — anlamı durum satırı + tooltip taşır */
|
|
2280
|
+
btn.innerHTML = on ? svgPause : svgPlay;
|
|
2281
|
+
btn.title = on ? 'Token sıkıştırmayı kapat' : 'Token sıkıştırmayı aç';
|
|
2282
|
+
btn.setAttribute('aria-label', btn.title);
|
|
2283
|
+
btn.addEventListener('click', async () => {
|
|
2284
|
+
const msg = $('#hrToggleMsg');
|
|
2285
|
+
btn.disabled = true;
|
|
2286
|
+
btn.innerHTML = '<span>…</span>';
|
|
2287
|
+
const r = await beast.headroomToggle(!on).catch(() => null);
|
|
2288
|
+
if (msg) {
|
|
2289
|
+
if (r && r.installing) {
|
|
2290
|
+
msg.textContent = 'Headroom arka planda kuruluyor — birkaç dakika sonra hazır olur, bu sekme açık kalabilir.';
|
|
2291
|
+
} else if (r && r.error) {
|
|
2292
|
+
msg.textContent = r.error;
|
|
2293
|
+
} else {
|
|
2294
|
+
msg.textContent = !on ? 'Açıldı — proxy hazır olunca trafiği yönlendirir' : 'Kapatıldı — trafik sağlayıcıya düz gidiyor';
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
setTimeout(() => renderHeadroomPane(), 2500);
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
/* açıkken istatistikler canlı aksın */
|
|
2302
|
+
if (hrStatsTimer) clearInterval(hrStatsTimer);
|
|
2303
|
+
hrStatsTimer = setInterval(async () => {
|
|
2304
|
+
if (pane.hidden) { clearInterval(hrStatsTimer); return; }
|
|
2305
|
+
const s2 = await beast.headroomStatus().catch(() => null);
|
|
2306
|
+
const cur = $('#hrToggle');
|
|
2307
|
+
if (s2 && cur && cur.disabled) return; /* toggle işlemi sürüyor */
|
|
2308
|
+
await renderHeadroomPane();
|
|
2309
|
+
}, 6000);
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2218
2312
|
async function renderInstallPane() {
|
|
2219
2313
|
const pane = $('#tab-install');
|
|
2220
2314
|
if (!pane) return;
|
|
@@ -2282,11 +2376,38 @@ async function renderInstallPane() {
|
|
|
2282
2376
|
'<div class="sub" style="font-size:11px">' + escapeHtml(r.detail || '') + (r.mb ? ' · ' + r.mb + ' MB' : '') + '</div>' +
|
|
2283
2377
|
bar(r) +
|
|
2284
2378
|
'</div>' +
|
|
2379
|
+
(r.canToggle
|
|
2380
|
+
? '<button class="btn hr-tgl" data-on="' + (r.toggleOn ? '1' : '0') + '" style="width:auto;padding:2px 12px;flex:none">' + (r.toggleOn ? 'KAPAT' : 'AÇ') + '</button>'
|
|
2381
|
+
: '') +
|
|
2382
|
+
(r.canInstall
|
|
2383
|
+
? '<button class="btn inst-kur" style="width:auto;padding:2px 12px;flex:none">KUR</button>'
|
|
2384
|
+
: '') +
|
|
2285
2385
|
badge(r.state) +
|
|
2286
2386
|
pctLabel(r) +
|
|
2287
2387
|
'</div>'
|
|
2288
2388
|
)).join('');
|
|
2289
|
-
|
|
2389
|
+
/* Headroom aç/kapa: toggle kurulum + proxy başlatmayı arka planda tetikler */
|
|
2390
|
+
el.querySelectorAll('.hr-tgl').forEach((b) => {
|
|
2391
|
+
b.addEventListener('click', async () => {
|
|
2392
|
+
const on = b.dataset.on !== '1';
|
|
2393
|
+
b.disabled = true;
|
|
2394
|
+
b.textContent = '…';
|
|
2395
|
+
const r = await beast.headroomToggle(on).catch(() => null);
|
|
2396
|
+
toast(r && r.error ? r.error : (r && r.installing ? 'Headroom kuruluyor — birkaç dakika sürer' : (on ? 'Headroom açıldı' : 'Headroom kapatıldı')));
|
|
2397
|
+
refresh();
|
|
2398
|
+
});
|
|
2399
|
+
});
|
|
2400
|
+
/* Android emülatörü: KUR → arka plan SDK kurulumu (JDK + cmdline-tools + imaj) */
|
|
2401
|
+
el.querySelectorAll('.inst-kur').forEach((b) => {
|
|
2402
|
+
b.addEventListener('click', async () => {
|
|
2403
|
+
b.disabled = true;
|
|
2404
|
+
b.textContent = '…';
|
|
2405
|
+
await beast.androidInstall().catch(() => {});
|
|
2406
|
+
toast('Android SDK arka planda kuruluyor — ~1.5GB, internete göre 5-20 dk sürer');
|
|
2407
|
+
refresh();
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
};
|
|
2290
2411
|
|
|
2291
2412
|
const refresh = async () => {
|
|
2292
2413
|
if (pane.hidden) return;
|
|
@@ -2852,67 +2973,6 @@ async function renderDashboardPane() {
|
|
|
2852
2973
|
pane.appendChild(list);
|
|
2853
2974
|
}
|
|
2854
2975
|
|
|
2855
|
-
/* ---------------- Limit: provider bazlı girdi limiti ---------------- */
|
|
2856
|
-
|
|
2857
|
-
async function renderLimitsPane() {
|
|
2858
|
-
/* Maliyet sekmesine GÖMÜLÜ çalışır (ayrı sekme kaldırıldı) */
|
|
2859
|
-
const pane = $('#tab-limits') || $('#limBox');
|
|
2860
|
-
if (!pane) return;
|
|
2861
|
-
const cur = await beast.getLimits().catch(() => ({ enabled: false, compress: true, default: 0, perProvider: {} }));
|
|
2862
|
-
const provs = [...new Set((state.models || []).map((m) => m.providerName))].sort((a, b) => a.localeCompare(b));
|
|
2863
|
-
|
|
2864
|
-
const provRows = provs.length
|
|
2865
|
-
? provs
|
|
2866
|
-
.map((p) => {
|
|
2867
|
-
const v = cur.perProvider[p] || 0;
|
|
2868
|
-
return `<div class="usage-row" style="align-items:center">
|
|
2869
|
-
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(p)}</span>
|
|
2870
|
-
<input class="inp lim-prov" data-prov="${escapeHtml(p)}" type="number" min="0" step="1000"
|
|
2871
|
-
value="${v || ''}" placeholder="0 (${_t('lim_unlimited')})" style="width:130px;flex:none;margin:0" />
|
|
2872
|
-
</div>`;
|
|
2873
|
-
})
|
|
2874
|
-
.join('')
|
|
2875
|
-
: '<p class="sub">' + _t('p_no_model') + '</p>';
|
|
2876
|
-
|
|
2877
|
-
pane.innerHTML =
|
|
2878
|
-
'<h2>' + _t('lim_h2') + '</h2>' +
|
|
2879
|
-
'<div class="sub">' + _t('lim_sub') + '</div>' +
|
|
2880
|
-
`<div class="fo-toggles" style="margin-top:10px">
|
|
2881
|
-
<label class="lock-row"><input type="checkbox" id="limOn" ${cur.enabled ? 'checked' : ''}/><span>${_t('lim_enabled')}</span></label>
|
|
2882
|
-
</div>
|
|
2883
|
-
<div id="limDetail" style="${cur.enabled ? '' : 'opacity:.45'}">
|
|
2884
|
-
<label class="lock-row"><input type="checkbox" id="limCompress" ${cur.compress ? 'checked' : ''}/><span>${_t('lim_compress')}</span></label>
|
|
2885
|
-
<div class="form-grid" style="grid-template-columns:1fr auto;align-items:center;margin-top:8px">
|
|
2886
|
-
<label class="mem-label" style="margin:0">${_t('lim_default')}</label>
|
|
2887
|
-
<input id="limDefault" class="inp" type="number" min="0" step="1000" value="${cur.default || ''}" placeholder="0" style="width:130px;flex:none;margin:0" />
|
|
2888
|
-
</div>
|
|
2889
|
-
<h2 style="margin-top:16px">${_t('lim_prov_h2')}</h2>
|
|
2890
|
-
<div class="sub">${_t('lim_prov_sub')}</div>
|
|
2891
|
-
<div style="margin-top:6px">${provRows}</div>
|
|
2892
|
-
<button id="limSave" class="btn ghost" style="margin-top:12px">${_t('lim_save')}</button>
|
|
2893
|
-
</div>`;
|
|
2894
|
-
|
|
2895
|
-
const det = pane.querySelector('#limDetail');
|
|
2896
|
-
pane.querySelector('#limOn').addEventListener('change', (e) => {
|
|
2897
|
-
det.style.opacity = e.target.checked ? '' : '.45';
|
|
2898
|
-
});
|
|
2899
|
-
|
|
2900
|
-
pane.querySelector('#limSave').addEventListener('click', async () => {
|
|
2901
|
-
const perProvider = {};
|
|
2902
|
-
for (const inp of pane.querySelectorAll('.lim-prov')) {
|
|
2903
|
-
const n = Math.round(Number(inp.value) || 0);
|
|
2904
|
-
if (n > 0) perProvider[inp.dataset.prov] = n;
|
|
2905
|
-
}
|
|
2906
|
-
await beast.setLimits({
|
|
2907
|
-
enabled: pane.querySelector('#limOn').checked,
|
|
2908
|
-
compress: pane.querySelector('#limCompress').checked,
|
|
2909
|
-
default: Math.round(Number(pane.querySelector('#limDefault').value) || 0),
|
|
2910
|
-
perProvider,
|
|
2911
|
-
});
|
|
2912
|
-
toast(_t('lim_saved_toast'));
|
|
2913
|
-
});
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
2976
|
/* ---------------- Güvenlik: ajan onay davranışı ---------------- */
|
|
2917
2977
|
|
|
2918
2978
|
async function renderSecurityPane() {
|
|
@@ -4982,6 +5042,19 @@ function onEvent(ev) {
|
|
|
4982
5042
|
return;
|
|
4983
5043
|
}
|
|
4984
5044
|
if (ev.type === 'install-progress') { updateInstallPct(ev); return; }
|
|
5045
|
+
/* Headroom durum değişimi → açık olan sekmeyi tazele (Token Sıkıştırma / Kurulum) */
|
|
5046
|
+
if (ev.type === 'headroom-status') {
|
|
5047
|
+
try {
|
|
5048
|
+
const iPane = document.getElementById('tab-install');
|
|
5049
|
+
const hPane = document.getElementById('tab-headroom');
|
|
5050
|
+
const open = (!iPane || iPane.hidden) && (!hPane || hPane.hidden) ? '' : (!hPane || hPane.hidden ? 'install' : 'headroom');
|
|
5051
|
+
if (open) {
|
|
5052
|
+
clearTimeout(hrPaneTimer);
|
|
5053
|
+
hrPaneTimer = setTimeout(() => { (open === 'headroom' ? renderHeadroomPane() : renderInstallPane()).catch(() => {}); }, 1500);
|
|
5054
|
+
}
|
|
5055
|
+
} catch {}
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
4985
5058
|
/* EMPATİ LOOP: proaktif bildirim (masaüstü) + sekme canlı yenileme.
|
|
4986
5059
|
Toast'ta yalnız haber başlığı — link/markdown kalabalığı chat mesajında. */
|
|
4987
5060
|
if (ev.type === 'proactive') {
|
|
@@ -5448,10 +5521,68 @@ function fmtWhen(iso) {
|
|
|
5448
5521
|
return d.toLocaleDateString([], { day: '2-digit', month: '2-digit' }) + ' ' + t;
|
|
5449
5522
|
}
|
|
5450
5523
|
|
|
5451
|
-
|
|
5452
|
-
|
|
5524
|
+
/* ---------- İZLEYİCİLER kenar pane (sohbet geçmişinin alt yarısı) ----------
|
|
5525
|
+
Kompakt liste: durum noktası + ad + bir sonraki tetik; satır → izleyici
|
|
5526
|
+
modalı; ▶/× mini kontroller. Yalnız agent modunda görünür (CSS). */
|
|
5527
|
+
let lastWatchPaneKey = '';
|
|
5528
|
+
async function renderWatchersPane(force) {
|
|
5529
|
+
const list = els.watchPaneList;
|
|
5530
|
+
if (!list) return;
|
|
5531
|
+
if (document.body.classList.contains('ide-mode') || document.body.classList.contains('studio-mode') || document.body.classList.contains('sandbox-mode')) return;
|
|
5453
5532
|
let rows = [];
|
|
5454
5533
|
try { rows = (await beast.watchersList()) || []; } catch {}
|
|
5534
|
+
const key = JSON.stringify(rows);
|
|
5535
|
+
if (!force && key === lastWatchPaneKey) return;
|
|
5536
|
+
lastWatchPaneKey = key;
|
|
5537
|
+
list.innerHTML = '';
|
|
5538
|
+
if (!rows.length) {
|
|
5539
|
+
list.innerHTML = '<div class="wp-empty">' + _t('w_empty') + '</div>';
|
|
5540
|
+
return;
|
|
5541
|
+
}
|
|
5542
|
+
for (const w of rows) {
|
|
5543
|
+
const row = document.createElement('div');
|
|
5544
|
+
row.className = 'wp-row' + (w.enabled ? '' : ' off');
|
|
5545
|
+
row.title = (w.kind === 'battery' ? 'pil yüzdesi' : w.kind === 'logs' ? 'log izleyici' : (w.path || w.re || w.url)) + ' · ' + opText(w) + ' · her ' + fmtEvery(w);
|
|
5546
|
+
row.innerHTML =
|
|
5547
|
+
`<span class="wp-dot"></span>` +
|
|
5548
|
+
`<div class="wp-main">` +
|
|
5549
|
+
`<div class="wp-name">${escapeHtml(w.name)}</div>` +
|
|
5550
|
+
`<div class="wp-meta">${opText(w)} · her ${escapeHtml(String(fmtEvery(w)))} · ${escapeHtml(fmtWhen(w.lastCheckAt))}` +
|
|
5551
|
+
(w.lastValue !== null && w.lastValue !== undefined ? ` · <b>${escapeHtml(String(w.lastValue).slice(0, 14))}</b>` : '') +
|
|
5552
|
+
`</div></div>`;
|
|
5553
|
+
const btns = document.createElement('div');
|
|
5554
|
+
btns.className = 'wp-btns';
|
|
5555
|
+
const t = document.createElement('button');
|
|
5556
|
+
t.className = 'wp-btn';
|
|
5557
|
+
t.textContent = w.enabled ? '❚❚' : '▶';
|
|
5558
|
+
t.title = w.enabled ? _t('cr_toggle_pause') : _t('cr_start');
|
|
5559
|
+
t.addEventListener('click', async (e) => {
|
|
5560
|
+
e.stopPropagation();
|
|
5561
|
+
await beast.watchersToggle(w.id);
|
|
5562
|
+
renderWatchersPane(true);
|
|
5563
|
+
renderWatchersModal(true);
|
|
5564
|
+
});
|
|
5565
|
+
const d = document.createElement('button');
|
|
5566
|
+
d.className = 'wp-btn del';
|
|
5567
|
+
d.textContent = '×';
|
|
5568
|
+
d.title = _t('cr_del');
|
|
5569
|
+
d.addEventListener('click', async (e) => {
|
|
5570
|
+
e.stopPropagation();
|
|
5571
|
+
await beast.watchersRemove(w.id);
|
|
5572
|
+
lastWatchPaneKey = '';
|
|
5573
|
+
renderWatchersPane(true);
|
|
5574
|
+
renderWatchersModal(true);
|
|
5575
|
+
});
|
|
5576
|
+
btns.append(t, d);
|
|
5577
|
+
row.appendChild(btns);
|
|
5578
|
+
row.addEventListener('click', () => openWatchModal());
|
|
5579
|
+
list.appendChild(row);
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
|
|
5583
|
+
let lastWatchKey = '';
|
|
5584
|
+
async function renderWatchersModal(force) {
|
|
5585
|
+
let rows = []; try { rows = (await beast.watchersList()) || []; } catch {}
|
|
5455
5586
|
/* canlı yenilemede içerik değişmediyse DOM'a dokunma — buton tıklamaları
|
|
5456
5587
|
yeniden kurulurken yutulmasın */
|
|
5457
5588
|
const key = JSON.stringify(rows);
|
|
@@ -5576,6 +5707,7 @@ function closeCronModal() {
|
|
|
5576
5707
|
async function openWatchModal() {
|
|
5577
5708
|
toggleMini(els.watchOverlay, false);
|
|
5578
5709
|
await renderWatchersModal(true); /* her basışta TAZE çekim */
|
|
5710
|
+
lastWatchPaneKey = ''; /* pane de bir sonraki tazelemede yeniden kurulur */
|
|
5579
5711
|
startMiniRefresh();
|
|
5580
5712
|
}
|
|
5581
5713
|
|
|
@@ -6290,6 +6422,7 @@ async function init() {
|
|
|
6290
6422
|
|
|
6291
6423
|
/* cron modülü — olay bağlama */
|
|
6292
6424
|
if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
|
|
6425
|
+
if (els.watchPaneOpen) els.watchPaneOpen.addEventListener('click', openWatchModal);
|
|
6293
6426
|
if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
|
|
6294
6427
|
if (els.watchClose) els.watchClose.addEventListener('click', closeWatchModal);
|
|
6295
6428
|
if (els.cronClose) els.cronClose.addEventListener('click', closeCronModal);
|
|
@@ -7139,6 +7272,10 @@ async function setIdeMode(on) {
|
|
|
7139
7272
|
bcBanner();
|
|
7140
7273
|
renderBcHistory(); /* soldaki geçmiş listesi: IDE'ye girişte tazelenir */
|
|
7141
7274
|
codeGutterRender(); /* dosya açık olmasa bile rakamlar görünür */
|
|
7275
|
+
/* Expo QR daha önce üretildiyse kart geri gelsin */
|
|
7276
|
+
beast.ideExpQr().then((r) => {
|
|
7277
|
+
if (r && r.ok && r.qr) bcQrShow(r.qr, r.url);
|
|
7278
|
+
}).catch(() => {});
|
|
7142
7279
|
} else {
|
|
7143
7280
|
/* terminal bölmeye alınmadan önce kapatılır, sonra ALT DOCK'a geri taşınır */
|
|
7144
7281
|
if (termOpen) termSetOpen(false);
|
|
@@ -8008,11 +8145,49 @@ async function undoTask(id) {
|
|
|
8008
8145
|
if (els.fileTasks) els.fileTasks.addEventListener('click', () => toggleTasksPanel());
|
|
8009
8146
|
|
|
8010
8147
|
$('#filePreview').addEventListener('click', async () => {
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8148
|
+
if (!ideModeOn()) await setIdeMode(true);
|
|
8149
|
+
/* proje tipi algıla: statik site ajan gerektirmez, dev-server projelerinde
|
|
8150
|
+
HER PROJE FARKLI KOMUTLA çalışır — komutu ajan package.json'a bakıp seçer */
|
|
8151
|
+
const info = await beast.ideProjInfo().catch(() => null);
|
|
8152
|
+
const kind = (info && info.kind) || 'unknown';
|
|
8153
|
+
if (kind === 'static') {
|
|
8154
|
+
const r = await beast.idePreview().catch(() => null);
|
|
8155
|
+
if (r && r.ok) return;
|
|
8015
8156
|
toast((r && r.error) || 'preview açılamadı');
|
|
8157
|
+
return;
|
|
8158
|
+
}
|
|
8159
|
+
/* ANDROID EMÜLATÖR: kuruluysa Expo uygulamaları emülatörde açılır —
|
|
8160
|
+
AVD arka planda başlatılır (kendi penceresi açılır), ajan expo'yu kaldırır */
|
|
8161
|
+
let ask = '';
|
|
8162
|
+
if (kind === 'expo' || kind === 'react-native') {
|
|
8163
|
+
const dev = await beast.androidStatus().catch(() => null);
|
|
8164
|
+
const hasEmu = dev && dev.ok && dev.sdk && Array.isArray(dev.avds) && dev.avds.length;
|
|
8165
|
+
if (hasEmu) {
|
|
8166
|
+
beast.androidStartAvd(dev.avds[0]).catch(() => {});
|
|
8167
|
+
toast('Android emülatörü başlatılıyor — ilk açılış 30-90 sn');
|
|
8168
|
+
ask =
|
|
8169
|
+
kind === 'expo'
|
|
8170
|
+
? 'Preview aç — bu klasör bir Expo/React Native projesi (app.json var). Android emülatörü başlatıldı; arka planda `npx expo start --android` çalıştır — uygulama emülatörde açılır (expo adb ile cihazı kendisi bulur). Çalışan http://localhost:PORT adresini raporda yaz.'
|
|
8171
|
+
: 'Preview aç — bu klasör bir React Native projesi. Android emülatörü hazır. Metro\u2019yu arka planda başlat (scripts.start varsa `npm start`) ve uygulamayı emülatöre kur: `npx react-native run-android`. Sunucu adresini raporda yaz.';
|
|
8172
|
+
}
|
|
8173
|
+
}
|
|
8174
|
+
if (!ask) {
|
|
8175
|
+
ask =
|
|
8176
|
+
kind === 'expo'
|
|
8177
|
+
? 'Preview aç — bu klasör bir Expo/React Native projesi (app.json var). Arka planda `npx expo start --tunnel` çalıştır (tunnel: telefon her ağdan bağlanabilir; web önizleme için `--web` de ekle). Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
|
|
8178
|
+
: kind === 'react-native'
|
|
8179
|
+
? 'Preview aç — bu klasör bir React Native projesi. Metro sunucusunu arka planda başlat: package.json scripts.start varsa `npm start`, yoksa `npx react-native start`. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
|
|
8180
|
+
: kind === 'web'
|
|
8181
|
+
? 'Preview aç — bu proje dev-server ister. Doğru komutu package.json scripts\'ından seç (dev/start/serve — `npm run dev` vb.) ve arka planda başlat. Sunucu kalkınca çalışan http://localhost:PORT adresini raporda yaz.'
|
|
8182
|
+
: 'Preview aç — bu projenin nasıl çalıştığını package.json / konfigürasyon dosyalarından anla, doğru dev komutunu arka planda başlat ve çalışan http://localhost:PORT adresini raporda yaz.';
|
|
8183
|
+
}
|
|
8184
|
+
if (els.bcInput) {
|
|
8185
|
+
els.bcInput.value = ask;
|
|
8186
|
+
bcInputResize();
|
|
8187
|
+
bcRunCurrent();
|
|
8188
|
+
toast('Ajan preview görevini aldı');
|
|
8189
|
+
} else {
|
|
8190
|
+
toast('Beast Code paneli yok');
|
|
8016
8191
|
}
|
|
8017
8192
|
});
|
|
8018
8193
|
if (els.codeSave) els.codeSave.addEventListener('click', codeSave);
|
|
@@ -8777,6 +8952,10 @@ function bcIngest(ev) {
|
|
|
8777
8952
|
/* iş bitince üretilen site/app dahili tarayıcıda CANLI açılır */
|
|
8778
8953
|
bcAutoPreview(ev.url);
|
|
8779
8954
|
break;
|
|
8955
|
+
case 'bc-exp-qr':
|
|
8956
|
+
/* Expo dev server exp:// adresi → telefonda Expo Go için QR kart */
|
|
8957
|
+
bcQrShow(ev.qr, ev.url);
|
|
8958
|
+
break;
|
|
8780
8959
|
case 'bc-mode':
|
|
8781
8960
|
/* OpenCode disiplini çalışma modu: başlıkta rozet + panelde bilgi satırı */
|
|
8782
8961
|
if (els.bcTitle) {
|
|
@@ -8904,6 +9083,33 @@ if (els.bcClear) els.bcClear.addEventListener('click', () => {
|
|
|
8904
9083
|
bcHideTodos();
|
|
8905
9084
|
bcFlushStream();
|
|
8906
9085
|
});
|
|
9086
|
+
|
|
9087
|
+
/* ---------- Expo Go QR kartı (yalnız Beast Code) ---------- */
|
|
9088
|
+
function bcQrShow(qr, url) {
|
|
9089
|
+
if (!els.bcQrCard || !ideModeOn() || !qr) return;
|
|
9090
|
+
if (els.bcQrImg) els.bcQrImg.src = qr;
|
|
9091
|
+
if (els.bcQrUrl) {
|
|
9092
|
+
els.bcQrUrl.textContent = url || '';
|
|
9093
|
+
els.bcQrUrl.title = url || '';
|
|
9094
|
+
}
|
|
9095
|
+
if (els.bcQrBtn) els.bcQrBtn.hidden = false;
|
|
9096
|
+
els.bcQrCard.hidden = false;
|
|
9097
|
+
}
|
|
9098
|
+
if (els.bcQrBtn) els.bcQrBtn.addEventListener('click', () => {
|
|
9099
|
+
if (!els.bcQrCard) return;
|
|
9100
|
+
if (els.bcQrCard.hidden) {
|
|
9101
|
+
beast.ideExpQr().then((r) => {
|
|
9102
|
+
if (r && r.ok && r.qr) bcQrShow(r.qr, r.url);
|
|
9103
|
+
else toast('QR henüz yok — ajan expo start çalıştırınca çıkar');
|
|
9104
|
+
}).catch(() => {});
|
|
9105
|
+
} else {
|
|
9106
|
+
els.bcQrCard.hidden = true;
|
|
9107
|
+
}
|
|
9108
|
+
});
|
|
9109
|
+
if (els.bcQrCard) {
|
|
9110
|
+
const qclose = els.bcQrCard.querySelector('#bcQrClose');
|
|
9111
|
+
if (qclose) qclose.addEventListener('click', () => { els.bcQrCard.hidden = true; });
|
|
9112
|
+
}
|
|
8907
9113
|
if (els.bcNew) els.bcNew.addEventListener('click', async () => {
|
|
8908
9114
|
if (bcRunning) { toast('Mesaj sürüyor — önce ■ ile durdur'); return; }
|
|
8909
9115
|
const r = await beast.beastcodeNew().catch(() => null);
|
package/src/renderer/style.css
CHANGED
|
@@ -112,6 +112,71 @@ body {
|
|
|
112
112
|
|
|
113
113
|
#sessList { flex: 1; overflow-y: auto; padding: 4px 8px; }
|
|
114
114
|
|
|
115
|
+
/* ---------- İZLEYİCİLER paneli (sol kenar çubuğu — sohbet geçmişinin alt yarısı) ----------
|
|
116
|
+
Yalnız agent (chat) modunda görünür; IDE/Studio/Sandbox'ta sessList ile birlikte gizli. */
|
|
117
|
+
#watchPane {
|
|
118
|
+
display: none;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
height: 36%;
|
|
121
|
+
min-height: 120px;
|
|
122
|
+
border-top: 1px solid var(--border);
|
|
123
|
+
background: var(--panel);
|
|
124
|
+
}
|
|
125
|
+
body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: flex; }
|
|
126
|
+
#watchPaneHead {
|
|
127
|
+
flex: none;
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 6px;
|
|
131
|
+
padding: 7px 10px;
|
|
132
|
+
border-bottom: 1px solid var(--border);
|
|
133
|
+
}
|
|
134
|
+
#watchPaneTitle { font-size: 10.5px; letter-spacing: 2px; color: var(--muted); font-weight: 800; }
|
|
135
|
+
#watchPaneHead button {
|
|
136
|
+
margin-left: auto;
|
|
137
|
+
background: none;
|
|
138
|
+
border: 1px solid var(--border);
|
|
139
|
+
border-radius: 7px;
|
|
140
|
+
color: var(--muted);
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
line-height: 1.2;
|
|
144
|
+
padding: 1px 7px;
|
|
145
|
+
}
|
|
146
|
+
#watchPaneHead button:hover { color: var(--text); background: var(--panel2); }
|
|
147
|
+
#watchPaneList { flex: 1; overflow-y: auto; padding: 4px; }
|
|
148
|
+
.wp-row {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
gap: 7px;
|
|
152
|
+
padding: 4px 6px;
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
font-size: 12px;
|
|
156
|
+
white-space: nowrap;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
}
|
|
159
|
+
.wp-row:hover { background: var(--panel2); }
|
|
160
|
+
.wp-dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
|
|
161
|
+
.wp-row.off .wp-dot { background: var(--muted); opacity: 0.5; }
|
|
162
|
+
.wp-main { flex: 1; min-width: 0; overflow: hidden; }
|
|
163
|
+
.wp-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; font-size: 12px; }
|
|
164
|
+
.wp-meta { color: var(--muted); font-size: 10px; overflow: hidden; text-overflow: ellipsis; }
|
|
165
|
+
.wp-btns { display: flex; gap: 4px; flex: none; }
|
|
166
|
+
.wp-btn {
|
|
167
|
+
background: none;
|
|
168
|
+
border: 1px solid var(--border);
|
|
169
|
+
border-radius: 6px;
|
|
170
|
+
color: var(--muted);
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
font-size: 10px;
|
|
173
|
+
padding: 1px 5px;
|
|
174
|
+
line-height: 1.3;
|
|
175
|
+
}
|
|
176
|
+
.wp-btn:hover { color: var(--text); background: var(--panel2); }
|
|
177
|
+
.wp-btn.del:hover { color: var(--err); border-color: var(--err); }
|
|
178
|
+
.wp-empty { color: var(--muted); font-size: 11.5px; padding: 10px; }
|
|
179
|
+
|
|
115
180
|
.sess {
|
|
116
181
|
display: flex;
|
|
117
182
|
align-items: center;
|
|
@@ -2315,8 +2380,61 @@ body.ide-mode #composerWrap { display: none; }
|
|
|
2315
2380
|
flex-direction: column;
|
|
2316
2381
|
overflow: hidden;
|
|
2317
2382
|
background: var(--bg);
|
|
2383
|
+
position: relative;
|
|
2318
2384
|
}
|
|
2319
2385
|
body.ide-mode #bcPanel { display: flex; }
|
|
2386
|
+
|
|
2387
|
+
/* ---------- Expo Go QR kartı (Beast Code) ---------- */
|
|
2388
|
+
#bcQrCard {
|
|
2389
|
+
position: absolute;
|
|
2390
|
+
right: 14px;
|
|
2391
|
+
bottom: 74px;
|
|
2392
|
+
z-index: 40;
|
|
2393
|
+
width: 232px;
|
|
2394
|
+
padding: 10px;
|
|
2395
|
+
background: var(--panel);
|
|
2396
|
+
border: 1px solid var(--border);
|
|
2397
|
+
border-radius: 12px;
|
|
2398
|
+
box-shadow: 0 10px 34px rgba(0, 0, 0, 0.45);
|
|
2399
|
+
}
|
|
2400
|
+
.bq-head {
|
|
2401
|
+
display: flex;
|
|
2402
|
+
align-items: center;
|
|
2403
|
+
justify-content: space-between;
|
|
2404
|
+
font-size: 10px;
|
|
2405
|
+
letter-spacing: 2px;
|
|
2406
|
+
font-weight: 800;
|
|
2407
|
+
color: var(--muted);
|
|
2408
|
+
margin-bottom: 6px;
|
|
2409
|
+
}
|
|
2410
|
+
#bcQrClose {
|
|
2411
|
+
background: none;
|
|
2412
|
+
border: none;
|
|
2413
|
+
color: var(--muted);
|
|
2414
|
+
font-size: 14px;
|
|
2415
|
+
cursor: pointer;
|
|
2416
|
+
line-height: 1;
|
|
2417
|
+
padding: 0 2px;
|
|
2418
|
+
}
|
|
2419
|
+
#bcQrClose:hover { color: var(--text); }
|
|
2420
|
+
#bcQrImg {
|
|
2421
|
+
width: 100%;
|
|
2422
|
+
display: block;
|
|
2423
|
+
border-radius: 8px;
|
|
2424
|
+
background: #fff;
|
|
2425
|
+
padding: 6px;
|
|
2426
|
+
image-rendering: pixelated;
|
|
2427
|
+
}
|
|
2428
|
+
#bcQrUrl {
|
|
2429
|
+
font-family: Consolas, monospace;
|
|
2430
|
+
font-size: 10px;
|
|
2431
|
+
color: var(--accent);
|
|
2432
|
+
margin-top: 6px;
|
|
2433
|
+
white-space: nowrap;
|
|
2434
|
+
overflow: hidden;
|
|
2435
|
+
text-overflow: ellipsis;
|
|
2436
|
+
}
|
|
2437
|
+
.bq-hint { font-size: 10.5px; color: var(--muted); margin-top: 4px; text-align: center; }
|
|
2320
2438
|
#bcHead {
|
|
2321
2439
|
flex: none;
|
|
2322
2440
|
display: flex;
|