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.
@@ -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
- let lastWatchKey = '';
5452
- async function renderWatchersModal(force) {
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
- const r = await beast.idePreview().catch(() => null);
8012
- if (r && r.ok) {
8013
- if (ideModeOn() === false) setIdeMode(true);
8014
- } else {
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);
@@ -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;