beast-agent 0.26.0 → 0.26.2

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.
@@ -471,7 +471,9 @@ async function renderSessions(list) {
471
471
  tgSet = new Set(await beast.tgListSessions());
472
472
  } catch {}
473
473
  els.sessList.innerHTML = '';
474
- /* aktif botun oturumları — botlar arası geçişte liste de o bota göre değişir */
474
+ /* aktif botun oturumları — botlar arası geçişte liste de o bota göre değişir.
475
+ KATI KURAL: her oturum yalnız BAĞLI OLDUĞU botun listesinde görünür;
476
+ bot kaydı olmayan (eski) oturumlar Beast (varsayılan sahip) altında görünür. */
475
477
  for (const s of list) {
476
478
  if ((s.botId || 'beast') !== activeBotId) continue;
477
479
  const row = document.createElement('div');
@@ -635,7 +637,7 @@ function switchTab(name) {
635
637
  document.querySelectorAll('#setTabs .tab').forEach((b) =>
636
638
  b.classList.toggle('active', b.dataset.tab === name)
637
639
  );
638
- for (const p of ['provider', 'fallout', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'events', 'cron', 'usage', 'logs', 'dash', 'limits', 'sec', 'proxy', 'update']) {
640
+ for (const p of ['provider', 'fallout', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'events', 'cron', 'usage', 'logs', 'dash', 'limits', 'sec', 'update']) {
639
641
  const el = $('#tab-' + p);
640
642
  if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
641
643
  }
@@ -646,7 +648,6 @@ function switchTab(name) {
646
648
  if (name === 'dash') renderDashboardPane();
647
649
  if (name === 'limits') renderLimitsPane();
648
650
  if (name === 'sec') renderSecurityPane();
649
- if (name === 'proxy') renderProxyPane();
650
651
  if (name === 'update') renderUpdatePane(true);
651
652
  if (name === 'agents') refreshAgentsPane();
652
653
  if (name === 'websearch') renderWebSearchPane();
@@ -816,7 +817,7 @@ async function renderProviderPane() {
816
817
 
817
818
  for (const m of state.models || []) {
818
819
  const row = document.createElement('div');
819
- row.className = 'prov-row' + (state.activeModel && state.activeModel.sel === m.sel ? ' active' : '');
820
+ row.className = 'prov-row' + (effectiveModelSel() === m.sel ? ' active' : '');
820
821
  row.innerHTML =
821
822
  `<span class="prov-name">${escapeHtml(m.providerName)}</span>` +
822
823
  `<span class="prov-model">${escapeHtml(m.model)}</span>` +
@@ -824,6 +825,7 @@ async function renderProviderPane() {
824
825
  `<button class="prov-del" title="${_t('p_del_title')}">✕</button>`;
825
826
  row.addEventListener('click', async () => {
826
827
  state = await beast.setModel(m.sel);
828
+ await refreshBots().catch(() => {});
827
829
  applyState();
828
830
  renderProviderPane();
829
831
  toast(_t('p_model_toast') + m.providerName + ' · ' + m.model);
@@ -1536,6 +1538,35 @@ async function renderIntegrationsPane() {
1536
1538
  <button id="tgAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1537
1539
  </div>
1538
1540
  <div class="sub" style="margin-top:8px">${_t('it_tg_note')}</div>
1541
+ </div>
1542
+
1543
+ <div class="wa-card" style="margin-top:14px">
1544
+ <div class="wa-head">
1545
+ <div class="wa-logo tg">D</div>
1546
+ <div>
1547
+ <div class="wa-title">Discord</div>
1548
+ <div class="wa-sub">${_t('it_dc_sub')}</div>
1549
+ </div>
1550
+ </div>
1551
+ <div class="wa-status"><span id="dcDot" class="wa-dot"></span><span id="dcStatText">—</span></div>
1552
+ <div id="dcUser" class="wa-user" hidden></div>
1553
+ <div class="form-grid" style="grid-template-columns:1fr auto;align-items:center">
1554
+ <input id="dcTokenInp" class="inp" type="password" style="margin:6px 0 0" placeholder="${_t('it_dc_token_ph')}" autocomplete="off" />
1555
+ <button id="dcSaveBtn" class="btn" style="margin-top:6px">${_t('it_tg_save')}</button>
1556
+ </div>
1557
+ <div class="wa-actions" style="margin-top:6px">
1558
+ <button id="dcStopBtn" class="btn ghost">${_t('it_disconnect')}</button>
1559
+ </div>
1560
+ <div class="divider"></div>
1561
+ <label class="mem-label" style="margin-top:0">${_t('it_allow_label')} — Discord</label>
1562
+ <div id="dcAllowChips" class="chips-inline"></div>
1563
+ <div class="form-grid" style="grid-template-columns:1.2fr 1fr 1fr auto;align-items:center">
1564
+ <input id="dcAllowNameInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_name_ph')}" autocomplete="off" />
1565
+ <input id="dcAllowIdInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_dc_id_ph')}" autocomplete="off" />
1566
+ <select id="dcAllowBotSel" class="perm-select" style="margin:6px 0 0;min-width:105px" title="${_t('bot_bind_title')}"></select>
1567
+ <button id="dcAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1568
+ </div>
1569
+ <div class="sub" style="margin-top:8px">${_t('it_dc_note')}</div>
1539
1570
  </div>`;
1540
1571
 
1541
1572
  const waGroupsOn = $('#waGroupsOn');
@@ -1598,6 +1629,29 @@ async function renderIntegrationsPane() {
1598
1629
  tgUI.user = ts.user || null;
1599
1630
  updateTgPane();
1600
1631
  } catch {}
1632
+
1633
+ /* DISCORD: token + allow list + durum */
1634
+ $('#dcSaveBtn').addEventListener('click', async () => {
1635
+ const tok = $('#dcTokenInp').value.trim();
1636
+ if (!tok) { toast(_t('it_dc_token_ph')); return; }
1637
+ toast(_t('it_dc_connecting'));
1638
+ const r = await beast.dcSetToken(tok).catch((e) => ({ status: 'error', error: String(e) }));
1639
+ $('#dcTokenInp').value = '';
1640
+ updateDcPane();
1641
+ toast(r.status === 'connected' ? 'Discord bağlı: ' + (r.user || '') : r.status === 'error' ? _t('it_dc_token_bad') : _t('it_dc_connecting'));
1642
+ });
1643
+ $('#dcStopBtn').addEventListener('click', async () => {
1644
+ await beast.dcStop().catch(() => {});
1645
+ updateDcPane();
1646
+ toast('Kesildi');
1647
+ });
1648
+ await renderDcAllow();
1649
+ try {
1650
+ const ds = await beast.dcGetStatus();
1651
+ dcUI.status = ds.status || 'disconnected';
1652
+ dcUI.user = ds.user || null;
1653
+ updateDcPane();
1654
+ } catch {}
1601
1655
  }
1602
1656
 
1603
1657
  /* ---------------- Olay Merkezi (ayrı sekme) ---------------- */
@@ -1791,134 +1845,8 @@ async function renderSecurityPane() {
1791
1845
  }
1792
1846
  }
1793
1847
 
1794
- /* ---------------- Proxy sekmesi ----------------
1795
- Kullanıcı yalnız IP/port/kullanıcı/şifre alanlarını doldurur; URL'i biz kurarız.
1796
- Eklenen her satır bir proxy — çoklu satır = Node tarafında round-robin rotasyon. */
1797
- let proxyDraft = null; // { enabled, items:[line,...] } — sekme açıkken çalışma kopyası
1798
-
1799
- function parseProxyLine(line) {
1800
- const m = String(line || '').trim().match(/^(https?|socks5):\/\/(?:([^:@/]+):([^@/]*)@)?([^:/]+):(\d+)$/i);
1801
- if (!m) return null;
1802
- return { proto: m[1].toLowerCase(), user: decodeURIComponent(m[2] || ''), pass: decodeURIComponent(m[3] || ''), host: m[4], port: m[5] };
1803
- }
1804
-
1805
- function proxyLineDisplay(line) {
1806
- const p = parseProxyLine(line);
1807
- if (!p) return escapeHtml(line);
1808
- const creds = p.user ? p.user + ':' + (p.pass ? '•••' : '') + '@' : '';
1809
- return `${p.proto}://${escapeHtml(creds)}${escapeHtml(p.host)}:${p.port}`;
1810
- }
1811
-
1812
- async function renderProxyPane() {
1813
- const pane = $('#tab-proxy');
1814
- if (!pane) return;
1815
- if (!proxyDraft) {
1816
- const [cfg, ws] = await Promise.all([
1817
- beast.proxyGet().catch(() => ({ enabled: false, urls: '' })),
1818
- beast.warpGet().catch(() => ({ enabled: false, running: false, installed: false })),
1819
- ]);
1820
- proxyDraft = {
1821
- enabled: !!cfg.enabled,
1822
- items: String(cfg.urls || '').split(/[\n,;]+/).map((s) => s.trim()).filter(Boolean),
1823
- warp: ws || {},
1824
- };
1825
- }
1826
- const warp = proxyDraft.warp || {};
1827
- const warpState = warp.enabled ? (warp.running ? _t('warp_state_on') : _t('warp_state_starting')) : _t('warp_state_off');
1828
-
1829
- const listHtml = proxyDraft.items.length
1830
- ? proxyDraft.items.map((line, i) =>
1831
- `<div class="px-row"><code>${proxyLineDisplay(line)}</code><button class="px-del" data-i="${i}" title="${_t('proxy_remove')}">✕</button></div>`
1832
- ).join('')
1833
- : `<div class="gh-empty" style="padding:10px">${_t('proxy_none')}</div>`;
1834
-
1835
- pane.innerHTML =
1836
- '<h2>' + _t('proxy_h2') + '</h2>' +
1837
- '<div class="sub">' + _t('proxy_sub') + '</div>' +
1838
- '<div class="warp-card">' +
1839
- `<div class="warp-head"><span>☁ Cloudflare WARP</span><span class="warp-state">${escapeHtml(warpState)}</span></div>` +
1840
- `<button id="pxWarp" class="btn ${warp.enabled ? 'ghost' : ''}">${warp.enabled ? _t('warp_off') : _t('warp_on')}</button>` +
1841
- `<div class="sub" style="margin-top:6px">${_t('warp_note')}</div>` +
1842
- '</div>' +
1843
- '<div class="fo-toggles" style="margin-top:12px">' +
1844
- `<label class="lock-row"><input type="checkbox" id="pxOn" ${proxyDraft.enabled ? 'checked' : ''}/><span>${_t('proxy_enabled')}</span></label>` +
1845
- '</div>' +
1846
- `<label class="mem-label">${_t('proxy_urls')}</label>` +
1847
- `<div id="pxList">${listHtml}</div>` +
1848
- '<div class="px-add-row">' +
1849
- `<select id="pxProto"><option value="http">http</option><option value="socks5">socks5</option></select>` +
1850
- `<input id="pxHost" placeholder="${_t('proxy_host')}" spellcheck="false"/>` +
1851
- `<input id="pxPort" placeholder="${_t('proxy_port')}" inputmode="numeric"/>` +
1852
- `<input id="pxUser" placeholder="${_t('proxy_user')}" autocomplete="off" spellcheck="false"/>` +
1853
- `<input id="pxPass" placeholder="${_t('proxy_pass')}" type="password" autocomplete="off"/>` +
1854
- `<button id="pxAdd" class="btn" style="margin-top:0">${_t('proxy_add')}</button>` +
1855
- '</div>' +
1856
- '<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:10px">' +
1857
- `<button id="pxSave" class="btn">${_t('proxy_save')}</button>` +
1858
- `<button id="pxTest" class="btn ghost">${_t('proxy_test')}</button>` +
1859
- '</div>' +
1860
- `<div id="pxTestOut" class="sub" style="margin-top:8px"></div>` +
1861
- '<div class="sub" style="margin-top:10px">' + _t('proxy_note') + '</div>';
1862
-
1863
- pane.querySelector('#pxOn').addEventListener('change', (e) => { proxyDraft.enabled = e.target.checked; });
1864
-
1865
- pane.querySelector('#pxWarp').addEventListener('click', async () => {
1866
- const btn = pane.querySelector('#pxWarp');
1867
- const st = pane.querySelector('#pxWarpState');
1868
- const want = !warp.enabled;
1869
- btn.disabled = true;
1870
- st.textContent = want ? '⏳ ' + _t('warp_state_starting') : '⏳ ' + _t('warp_state_off');
1871
- const r = await beast.warpSet(want).catch((e) => ({ ok: false, error: String(e) }));
1872
- if (!(r && r.ok)) {
1873
- btn.disabled = false;
1874
- st.textContent = '⚠ ' + ((r && r.error) || 'hata');
1875
- return;
1876
- }
1877
- proxyDraft = null; // listeyi (socks satırı eklenmiş/çıkarılmış) tazele
1878
- renderProxyPane();
1879
- });
1880
-
1881
- const collect = () => {
1882
- const host = pane.querySelector('#pxHost').value.trim();
1883
- const port = pane.querySelector('#pxPort').value.trim();
1884
- const user = pane.querySelector('#pxUser').value.trim();
1885
- const pass = pane.querySelector('#pxPass').value;
1886
- const proto = pane.querySelector('#pxProto').value || 'http';
1887
- if (!host || !/^\d{1,5}$/.test(port)) { toast(_t('proxy_invalid')); return null; }
1888
- const creds = user ? encodeURIComponent(user) + (pass ? ':' + encodeURIComponent(pass) : '') + '@' : '';
1889
- return `${proto}://${creds}${host}:${port}`;
1890
- };
1891
- pane.querySelector('#pxAdd').addEventListener('click', () => {
1892
- const line = collect();
1893
- if (!line) return;
1894
- if (proxyDraft.items.includes(line)) { toast(_t('proxy_dup')); return; }
1895
- proxyDraft.items.push(line);
1896
- renderProxyPane();
1897
- });
1898
- pane.querySelectorAll('.px-del').forEach((b) =>
1899
- b.addEventListener('click', () => {
1900
- proxyDraft.items.splice(Number(b.dataset.i), 1);
1901
- renderProxyPane();
1902
- })
1903
- );
1904
-
1905
- pane.querySelector('#pxSave').addEventListener('click', async () => {
1906
- const r = await beast.proxySet({ enabled: proxyDraft.enabled, urls: proxyDraft.items.join('\n') }).catch(() => null);
1907
- if (r && r.ok) toast(_t('proxy_saved'));
1908
- else toast((r && r.error) || 'hata');
1909
- });
1910
- pane.querySelector('#pxTest').addEventListener('click', async () => {
1911
- const out = pane.querySelector('#pxTestOut');
1912
- out.textContent = '⏳ ' + _t('proxy_testing');
1913
- /* önce kaydet — test, kayıtlı ayarla çalışır */
1914
- await beast.proxySet({ enabled: proxyDraft.enabled, urls: proxyDraft.items.join('\n') }).catch(() => null);
1915
- const r = await beast.proxyTest().catch((e) => ({ ok: false, error: String(e) }));
1916
- if (r && r.ok) out.innerHTML = `✅ ${_t('proxy_test_ok')} <b>${escapeHtml(r.ip || '?')}</b> · ${escapeHtml(r.proxy || '')} · ${r.ms || '?'}ms`;
1917
- else out.innerHTML = `⚠ ${_t('proxy_test_fail')} ${escapeHtml((r && r.error) || '?')}`;
1918
- });
1919
- }
1920
-
1921
1848
  /* ---------------- Update: sürüm kontrol + otomatik güncelleme ---------------- */
1849
+
1922
1850
  let updatePaneTimer = null;
1923
1851
 
1924
1852
  function renderUpdateStateHtml(st) {
@@ -2566,6 +2494,199 @@ async function renderTgAllow() {
2566
2494
  }
2567
2495
  }
2568
2496
 
2497
+ /* ---------------- Discord izin listesi + durum — TG ile aynı mantık ---------------- */
2498
+
2499
+ const dcUI = { status: 'disconnected', user: null };
2500
+ const DC_STATUS_TEXT = {
2501
+ disconnected: 'Bağlı değil',
2502
+ connecting: 'Bağlanıyor…',
2503
+ connected: 'Bağlı',
2504
+ error: 'Hata — tokenı kontrol et',
2505
+ };
2506
+
2507
+ function onDcEvent(ev) {
2508
+ if (ev.type !== 'status') return;
2509
+ dcUI.status = ev.status;
2510
+ if (ev.user) dcUI.user = ev.user;
2511
+ updateDcPane();
2512
+ }
2513
+
2514
+ function updateDcPane() {
2515
+ const pane = $('#tab-integrations');
2516
+ const dot = pane && pane.querySelector('#dcDot');
2517
+ if (!dot) return;
2518
+ dot.className = 'wa-dot' + (dcUI.status === 'connected' ? ' on' : dcUI.status === 'error' ? ' qr' : '');
2519
+ pane.querySelector('#dcStatText').textContent = DC_STATUS_TEXT[dcUI.status] || dcUI.status;
2520
+ const u = pane.querySelector('#dcUser');
2521
+ u.hidden = !(dcUI.status === 'connected' && dcUI.user);
2522
+ u.textContent = dcUI.user ? '🤖 ' + dcUI.user : '';
2523
+ }
2524
+
2525
+ async function renderDcAllow() {
2526
+ const wrap = $('#dcAllowChips');
2527
+ if (!wrap) return;
2528
+ const list = await beast.dcGetAllow();
2529
+ let botChoices = [];
2530
+ try { botChoices = (await beast.botsList()) || []; } catch {}
2531
+ const dcLabel = (e) => {
2532
+ if (e === '*') return '* herkes';
2533
+ if (typeof e === 'string') return e;
2534
+ const name = String((e && e.name) || '').trim();
2535
+ const id = String((e && e.id) || '');
2536
+ const bot = e && e.bot_id ? botChoices.find((bb) => bb.id === e.bot_id) : null;
2537
+ return (name ? name + ' ' : '') + id + (bot ? ' → ' + bot.name : '');
2538
+ };
2539
+ wrap.innerHTML = '';
2540
+ if (!list.length) wrap.innerHTML = '<span class="sub">— boş —</span>';
2541
+ list.forEach((entry, idx) => {
2542
+ const curPerm = (typeof entry === 'object' && entry.perm) || (entry && entry.lockdown ? 'chat' : 'all');
2543
+ const c = document.createElement('span');
2544
+ c.className = 'chip';
2545
+ c.style.margin = '0 6px 6px 0';
2546
+ const txt = document.createElement('span');
2547
+ txt.className = 'chip-txt';
2548
+ txt.textContent = dcLabel(entry);
2549
+ c.appendChild(txt);
2550
+ if (entry !== '*') {
2551
+ const selP = document.createElement('select');
2552
+ selP.className = 'perm-select';
2553
+ selP.title = _t('wa_perm_title');
2554
+ const PERMS = [
2555
+ ['all', _t('wa_perm_all')],
2556
+ ['web', 'web'],
2557
+ ['read', _t('wa_perm_read')],
2558
+ ['chat', _t('wa_perm_chat')],
2559
+ ];
2560
+ for (const [v, lbl] of PERMS) {
2561
+ const o = document.createElement('option');
2562
+ o.value = v;
2563
+ o.textContent = lbl;
2564
+ if (curPerm === v) o.selected = true;
2565
+ selP.appendChild(o);
2566
+ }
2567
+ selP.addEventListener('change', async () => {
2568
+ const cur = await beast.dcGetAllow();
2569
+ const next = cur.map((e, i) => {
2570
+ if (i !== idx) return e;
2571
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2572
+ base.lockdown = selP.value === 'chat';
2573
+ base.perm = selP.value;
2574
+ return base;
2575
+ });
2576
+ await beast.dcSetAllow(next);
2577
+ renderDcAllow();
2578
+ toast(dcLabel(entry) + ': ' + selP.selectedOptions[0].textContent);
2579
+ });
2580
+ c.appendChild(selP);
2581
+
2582
+ if (botChoices.length) {
2583
+ const sel = document.createElement('select');
2584
+ sel.className = 'perm-select';
2585
+ sel.title = _t('bot_bind_title');
2586
+ const empty = document.createElement('option');
2587
+ empty.value = '';
2588
+ empty.textContent = _t('bot_sel_empty');
2589
+ sel.appendChild(empty);
2590
+ for (const bb of botChoices) {
2591
+ const o = document.createElement('option');
2592
+ o.value = bb.id;
2593
+ o.textContent = `${bb.icon} ${bb.name}`;
2594
+ if ((typeof entry === 'object' && entry.bot_id) === bb.id) o.selected = true;
2595
+ sel.appendChild(o);
2596
+ }
2597
+ sel.addEventListener('change', async () => {
2598
+ const cur = await beast.dcGetAllow();
2599
+ const next = cur.map((e, i) => {
2600
+ if (i !== idx) return e;
2601
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2602
+ base.bot_id = sel.value || undefined;
2603
+ return base;
2604
+ });
2605
+ await beast.dcSetAllow(next);
2606
+ renderDcAllow();
2607
+ const bt = botChoices.find((bb) => bb.id === sel.value);
2608
+ toast(dcLabel(entry) + ' → ' + (bt ? bt.name : '?'));
2609
+ });
2610
+ c.appendChild(sel);
2611
+ }
2612
+
2613
+ const isOwnerEntry = typeof entry === 'object' && !!entry.owner;
2614
+ const ownerBtn = document.createElement('span');
2615
+ ownerBtn.className = 'lk';
2616
+ ownerBtn.style.cssText = 'font-size:11px;padding:1px 6px;' + (isOwnerEntry ? 'color:#c9a227;font-weight:800' : '');
2617
+ ownerBtn.title = isOwnerEntry ? _t('wa_owner_title_on') : _t('wa_owner_title_off');
2618
+ ownerBtn.textContent = isOwnerEntry ? _t('wa_owner_on') : _t('wa_owner_off');
2619
+ ownerBtn.addEventListener('click', async () => {
2620
+ if (isOwnerEntry) return;
2621
+ const cur = await beast.dcGetAllow();
2622
+ const next = cur.map((e, i) => {
2623
+ if (typeof e === 'object' && e && e.owner) return { ...e, owner: false };
2624
+ if (i === idx) {
2625
+ const obj = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2626
+ obj.owner = true;
2627
+ return obj;
2628
+ }
2629
+ return e;
2630
+ });
2631
+ const tgt = next[idx];
2632
+ await beast.dcSetAllow(next);
2633
+ renderDcAllow();
2634
+ toast('Sahip: ' + dcLabel(tgt));
2635
+ });
2636
+ c.appendChild(ownerBtn);
2637
+ }
2638
+ const x = document.createElement('span');
2639
+ x.className = 'x';
2640
+ x.textContent = '×';
2641
+ x.addEventListener('click', async () => {
2642
+ const next = (await beast.dcGetAllow()).filter((_v, i) => i !== idx);
2643
+ await beast.dcSetAllow(next);
2644
+ renderDcAllow();
2645
+ toast('Kaldırıldı: ' + dcLabel(entry));
2646
+ });
2647
+ c.appendChild(x);
2648
+ wrap.appendChild(c);
2649
+ });
2650
+
2651
+ const inp = $('#dcAllowIdInp');
2652
+ const nameInp = $('#dcAllowNameInp');
2653
+ const add = $('#dcAllowAdd');
2654
+ const botSel = $('#dcAllowBotSel');
2655
+ if (botSel) {
2656
+ const prev = botSel.value;
2657
+ botSel.innerHTML =
2658
+ `<option value="">${_t('bot_sel_empty')}</option>` +
2659
+ botChoices
2660
+ .map((bb) => `<option value="${bb.id}">${bb.icon} ${escapeHtml(bb.name)}${bb.admin ? ' (admin)' : ''}</option>`)
2661
+ .join('');
2662
+ if (prev) botSel.value = prev;
2663
+ }
2664
+ if (!add.dataset.bound) {
2665
+ add.dataset.bound = '1';
2666
+ const addEntry = async () => {
2667
+ let v = inp.value.trim();
2668
+ if (!v) return;
2669
+ const name = nameInp.value.trim().slice(0, 40);
2670
+ if (!name) { toast('İsim zorunlu — kimin yazdığını bilmek için'); nameInp.focus(); return; }
2671
+ v = v.startsWith('@') ? '@' + v.slice(1).replace(/[^\w]/g, '') : v.replace(/[^\d]/g, '');
2672
+ if (!v) { toast(_t('it_dc_id_ph')); return; }
2673
+ const botId = botSel && botSel.value ? { bot_id: botSel.value } : {};
2674
+ const next = [...(await beast.dcGetAllow()), { id: v, name, ...botId }];
2675
+ if (!next.some((e) => e && typeof e === 'object' && e.owner)) {
2676
+ if (next.length && typeof next[0] === 'object') next[0].owner = true;
2677
+ }
2678
+ await beast.dcSetAllow(next);
2679
+ inp.value = '';
2680
+ nameInp.value = '';
2681
+ renderDcAllow();
2682
+ toast('Eklendi: ' + name);
2683
+ };
2684
+ add.addEventListener('click', addEntry);
2685
+ inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2686
+ nameInp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2687
+ }
2688
+ }
2689
+
2569
2690
  /* ---------------- BOT SİSTEMİ (BÖLÜM 2-3-4) ----------------
2570
2691
  Sol panel altında bot kartları; her bot için sohbet geçmişi + sekmeli yönetim.
2571
2692
  İlk bot hep Beast (admin, silinemez). Max 5 bot. */
@@ -2601,6 +2722,8 @@ async function refreshBots() {
2601
2722
  /* BOT PICKER SENKRONU: liste her yenilendiğinde üstteki rozet de aktif bota
2602
2723
  ayarlanır — startup yarışında (liste boşken çizilen) eski rozet düzelir */
2603
2724
  updateBotChip();
2725
+ /* model picker da aktif botun kendi modelini göstersin */
2726
+ if (state) applyState();
2604
2727
  if (!$('#botOverlay').hidden) renderBotPage();
2605
2728
  }
2606
2729
 
@@ -2664,6 +2787,8 @@ async function switchBot(id) {
2664
2787
  if (list.length) await openSession(list[0].id);
2665
2788
  else { const v = await beast.createSession(); await openSession(v.id); }
2666
2789
  } catch {}
2790
+ /* picker, aktif botun kendi modelini göstersin */
2791
+ applyState();
2667
2792
  toast((b.icon || '🤖') + ' ' + b.name + (b.admin ? '' : ' — ' + _t('bot_switched')));
2668
2793
  }
2669
2794
 
@@ -2894,6 +3019,15 @@ function renderBotSettings(pane, b) {
2894
3019
  const numRows = (b.numbers || [])
2895
3020
  .map((n) => `<div class="bot-num-row" data-num="${n.num}"><span class="n">+${escapeHtml(n.num)}${n.name ? ' · ' + escapeHtml(n.name) : ''}</span><span class="x" title="${_t('bot_num_del')}">×</span></div>`)
2896
3021
  .join('');
3022
+ /* bot bazlı model seçenekleri: ana picker'daki listeyle aynı (state.models) */
3023
+ const modelOpts = ['<option value="">' + escapeHtml(_t('bot_model_global')) + '</option>']
3024
+ .concat(
3025
+ (state.models || []).map((m) => {
3026
+ const s = m.sel || (m.providerId || '') + '::' + (m.model || '');
3027
+ return `<option value="${escapeHtml(s)}" ${b.model === s ? 'selected' : ''}>${escapeHtml(m.providerName || m.providerId || '')} · ${escapeHtml(m.model || '')}</option>`;
3028
+ })
3029
+ )
3030
+ .join('');
2897
3031
  /* Numara ekleme KALDIRILDI — girişler yalnız Ayarlar → Entegrasyonlar (WhatsApp
2898
3032
  izin listesi) üzerinden yapılır. Bu bölüm salt-okunur bağlı-numara listesidir. */
2899
3033
  f.innerHTML = `
@@ -2916,7 +3050,10 @@ function renderBotSettings(pane, b) {
2916
3050
  <div class="bot-checks" id="bPerm">${permChecks}</div>
2917
3051
  <div class="sub" style="margin-top:4px">${_t('bot_perm_note')}</div>
2918
3052
  <label class="mem-label">${_t('bot_skills')}</label>
2919
- <div class="bot-checks" id="bSkills">${skillChecks}</div>`}
3053
+ <div class="bot-checks" id="bSkills">${skillChecks}</div>
3054
+ <label class="mem-label">${_t('bot_model')}</label>
3055
+ <select id="bModel" class="inp">${modelOpts}</select>
3056
+ <div class="sub" style="margin-top:4px">${_t('bot_model_note')}</div>`}
2920
3057
  <label class="mem-label">${_t('bot_browser')}</label>
2921
3058
  <div class="bot-checks" style="margin-bottom:6px">
2922
3059
  <label><input type="checkbox" id="bExtBrowser" ${b.extBrowser ? 'checked' : ''}/> ${_t('bot_ext_browser')}</label>
@@ -2991,6 +3128,8 @@ function renderBotSettings(pane, b) {
2991
3128
  const sk = {};
2992
3129
  f.querySelectorAll('#bSkills input').forEach((c) => { sk[c.dataset.skill] = c.checked; });
2993
3130
  patch.skills = sk;
3131
+ const bm = $('#bModel');
3132
+ if (bm) patch.model = bm.value;
2994
3133
  }
2995
3134
  const r = await beast.botsUpdate(b.id, patch);
2996
3135
  if (r.ok) { toast(_t('bot_saved')); await refreshBots(); renderBotPage(); }
@@ -3214,6 +3353,39 @@ function fmtAgo(iso) {
3214
3353
  return s >= 60 ? Math.floor(s / 60) + 'dk' : s + 'sn';
3215
3354
  }
3216
3355
 
3356
+ /* canlı süre sayacı: 0:43 · 2:05 · 1:02:15 biçimi (koşan ajan kartlarında) */
3357
+ function fmtElapsed(ms) {
3358
+ const s = Math.max(0, Math.floor(ms / 1000));
3359
+ const h = Math.floor(s / 3600);
3360
+ const m = Math.floor((s % 3600) / 60);
3361
+ const sec = s % 60;
3362
+ const p = (n) => String(n).padStart(2, '0');
3363
+ return h > 0 ? `${h}:${p(m)}:${p(sec)}` : `${m}:${p(sec)}`;
3364
+ }
3365
+
3366
+ /* koşan ajanların ⏱ sayaçları — saniyede bir yalnızca sayaç span'leri güncellenir
3367
+ (tüm rail yeniden çizilmez). Koşan kart kalmayınca interval kapanır. */
3368
+ let agentTimerInt = null;
3369
+ function ensureAgentTimers() {
3370
+ const any = !!document.querySelector('.agent-timer[data-start]');
3371
+ if (any && !agentTimerInt) {
3372
+ agentTimerInt = setInterval(() => {
3373
+ document.querySelectorAll('.agent-timer[data-start]').forEach((el) => {
3374
+ const t0 = Date.parse(el.dataset.start || '');
3375
+ if (t0) el.textContent = fmtElapsed(Date.now() - t0);
3376
+ });
3377
+ }, 1000);
3378
+ } else if (!any && agentTimerInt) {
3379
+ clearInterval(agentTimerInt);
3380
+ agentTimerInt = null;
3381
+ }
3382
+ }
3383
+ function agentTimerHtml(startedAt) {
3384
+ const t0 = Date.parse(startedAt || '');
3385
+ const val = t0 ? fmtElapsed(Date.now() - t0) : '0:00';
3386
+ return `<span class="agent-timer" data-start="${escapeHtml(startedAt || '')}">⏱ ${val}</span>`;
3387
+ }
3388
+
3217
3389
  /* sağ panel — paralel ajanların arka plan işleri */
3218
3390
  function renderAgentRail() {
3219
3391
  const list = els.railList;
@@ -3244,7 +3416,7 @@ function renderAgentRail() {
3244
3416
  `<span class="ag-dot"></span>` +
3245
3417
  `<span class="sess-title">${escapeHtml(j.title)}</span>` +
3246
3418
  (j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
3247
- `<span class="rj-time">${j.status === 'running' ? when + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
3419
+ `<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
3248
3420
  (j.status === 'running'
3249
3421
  ? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
3250
3422
  : `<button class="rj-cancel" title="${_t('ag_delete')}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>`) +
@@ -3289,6 +3461,7 @@ function renderAgentRail() {
3289
3461
  /* açık sohbet dökümünü re-render sonrası GERİ GETİR (kapanma bug'ının fix'i) */
3290
3462
  if (open && agentState.chatOpen.has(j.id)) maybeLoadRailChat(j.id);
3291
3463
  }
3464
+ ensureAgentTimers();
3292
3465
  }
3293
3466
 
3294
3467
  /* re-render storm'da IPC fırtınası olmasın: döküm 2 sn'de bir tazelenir */
@@ -3374,7 +3547,7 @@ function renderAgentsPane() {
3374
3547
  `<span class="ag-dot"></span>` +
3375
3548
  `<span class="ag-title">${escapeHtml(j.title)}</span>` +
3376
3549
  `<span class="ag-st">${agStText(j.status) || j.status}</span>` +
3377
- `<span class="ag-time">${when}${j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
3550
+ `<span class="ag-time">${when}${j.status === 'running' ? ' · ' + agentTimerHtml(j.startedAt) : j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
3378
3551
  (j.status === 'running'
3379
3552
  ? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
3380
3553
  : ``) +
@@ -3412,16 +3585,8 @@ function renderAgentsPane() {
3412
3585
  list.appendChild(card);
3413
3586
  }
3414
3587
 
3415
- /* canlı süre sayacı */
3416
- clearInterval(agentState.tick);
3417
- agentState.tick = setInterval(() => {
3418
- if (setTab !== 'agents') { clearInterval(agentState.tick); return; }
3419
- for (const el of document.querySelectorAll('.agent-card.st-running .ag-time')) {
3420
- /* sadece saati yenile */
3421
- const t = el.textContent.split(' ')[0];
3422
- el.textContent = `${t} · ${_t('ag_working')}…`;
3423
- }
3424
- }, 5000);
3588
+ /* canlı süre sayacı — saniyede bir yalnız ⏱ sayaç span'leri güncellenir */
3589
+ ensureAgentTimers();
3425
3590
  }
3426
3591
 
3427
3592
  /* ---------------- events from engine ---------------- */
@@ -3522,6 +3687,7 @@ function onEvent(ev) {
3522
3687
  case 'browser': {
3523
3688
  /* visible=false → ajan tarayıcıyı GİZLİ kullanıyor: UI yer açmaz */
3524
3689
  const shown = !!ev.open && ev.visible !== false;
3690
+ const wasShown = document.body.classList.contains('browser-open');
3525
3691
  document.body.classList.toggle('browser-open', shown);
3526
3692
  if (shown && ev.width) document.body.style.setProperty('--bw', ev.width + 'px');
3527
3693
  els.browserBar.hidden = !shown;
@@ -3529,11 +3695,13 @@ function onEvent(ev) {
3529
3695
  /* terminal de sağ dock'u kullanır — tarayıcı açılınca yerini bırak */
3530
3696
  if (shown && termOpen) termSetOpen(false);
3531
3697
  /* #19 tarayıcı açılınca paralel ajan konsolu (sağ panel) yerini bırakır;
3532
- kapanınca önceki durumuna döner — istenirse railBtn ile elle açılır */
3698
+ kapanınca önceki durumuna döner — istenirse railBtn ile elle açılır.
3699
+ GİZLİ ajan gezinmeleri (shown=false, wasShown=false) rail'e DOKUNMAZ —
3700
+ aksi halde paralel ajan konsolu kendi kendine kapanırdı */
3533
3701
  if (shown) {
3534
3702
  railPrefBeforeBrowser = !document.body.classList.contains('rail-hidden');
3535
3703
  toggleRail(true);
3536
- } else {
3704
+ } else if (wasShown) {
3537
3705
  toggleRail(!railPrefBeforeBrowser);
3538
3706
  }
3539
3707
  /* IDE modunda tarayıcı açılır/kapanır/genişlik değişirse row yeniden bölünür —
@@ -3862,15 +4030,26 @@ async function openCronModal() {
3862
4030
 
3863
4031
  /* ---------------- state / controls ---------------- */
3864
4032
 
4033
+ /* AKTİF BOTUN etkili model seçimi: müşteri botun kendi modeli varsa o,
4034
+ yoksa (ve admin bot daysa) global picker seçimi. */
4035
+ function effectiveModelSel() {
4036
+ const b = botsCache.find((x) => x.id === activeBotId);
4037
+ return b && !b.admin && b.model ? b.model : (state.activeModel && state.activeModel.sel) || '';
4038
+ }
4039
+
3865
4040
  function applyState() {
3866
4041
  if (!state) return;
3867
- els.modelBtnLabel.textContent = state.activeModel
3868
- ? `${state.activeModel.providerName} · ${state.activeModel.model}`
3869
- : 'Model seçilmedi';
4042
+ const botSel = activeBotId && botsCache.length ? effectiveModelSel() : (state.activeModel && state.activeModel.sel);
4043
+ const bm = botSel ? (state.models || []).find((x) => x.sel === botSel) : null;
4044
+ els.modelBtnLabel.textContent = bm
4045
+ ? `${bm.providerName} · ${bm.model}`
4046
+ : state.activeModel
4047
+ ? `${state.activeModel.providerName} · ${state.activeModel.model}`
4048
+ : 'Model seçilmedi';
3870
4049
  els.thinkBtnLabel.innerHTML =
3871
- '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 2A2.5 2.5 0 0 0 7 4.5v.55A3.5 3.5 0 0 0 4.5 8.5c0 .74.23 1.43.62 2A3.5 3.5 0 0 0 4 13.5 3.5 3.5 0 0 0 7 16.95v.55A2.5 2.5 0 0 0 9.5 20a2.5 2.5 0 0 0 2.5-2.5v-13A2.5 2.5 0 0 0 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 1 17 4.5v.55a3.5 3.5 0 0 1 2.5 3.45c0 .74-.23 1.43-.62 2a3.5 3.5 0 0 1 1.12 3 3.5 3.5 0 0 1-3 3.45v.55A2.5 2.5 0 0 1 14.5 20 2.5 2.5 0 0 1 12 17.5v-13A2.5 2.5 0 0 1 14.5 2z"/></svg>';
4050
+ '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 2A2.5 2.5 0 0 0 7 4.5v.55A3.5 3.5 0 0 0 4.5 8.5c0 .74.23 1.43.62 2A3.5 3.5 0 0 0 4 13.5 3.5 3.5 0 0 0 7 16.95v.55A2.5 2.5 0 0 0 9.5 20a2.5 2.5 0 0 0 2.5-2.5v-13A2.5 2.5 0 0 0 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 1 17 4.5v.55a3.5 3.5 0 0 1 2.5 3.45c0 .74-.23 1.43-.62 2a3.5 3.5 0 0 1-1.12 3 3.5 3.5 0 0 1-3 3.45v.55A2.5 2.5 0 0 1 14.5 20 2.5 2.5 0 0 1 12 17.5v-13A2.5 2.5 0 0 1 14.5 2z"/></svg>';
3872
4051
  renderModelMenu();
3873
- }
4052
+ }
3874
4053
 
3875
4054
  /* düşünme (reasoning) seviyesi picker — gerçek API değerleri */
3876
4055
  const THINK_UI_LEVELS = [
@@ -3927,7 +4106,7 @@ function renderModelMenu() {
3927
4106
  els.modelList.innerHTML = '';
3928
4107
  for (const m of list) {
3929
4108
  const item = document.createElement('div');
3930
- item.className = 'dd-item' + (state.activeModel && state.activeModel.sel === m.sel ? ' active' : '');
4109
+ item.className = 'dd-item' + (effectiveModelSel() && effectiveModelSel() === m.sel ? ' active' : '');
3931
4110
  item.innerHTML =
3932
4111
  `<span class="dn">${escapeHtml(m.providerName)}</span>` +
3933
4112
  `<span class="dm">${escapeHtml(m.model)}</span>` +
@@ -3935,6 +4114,7 @@ function renderModelMenu() {
3935
4114
  item.addEventListener('click', async () => {
3936
4115
  state = await beast.setModel(m.sel);
3937
4116
  closeModelMenu();
4117
+ await refreshBots().catch(() => {}); // bot modeli değiştiyse botsCache tazelensin
3938
4118
  applyState();
3939
4119
  });
3940
4120
  els.modelList.appendChild(item);
@@ -4386,6 +4566,7 @@ async function init() {
4386
4566
 
4387
4567
  beast.onWaEvent(onWaEvent);
4388
4568
  beast.onTgEvent(onTgEvent);
4569
+ beast.onDcEvent(onDcEvent);
4389
4570
 
4390
4571
  els.gearBtn.addEventListener('click', openSettings);
4391
4572
  els.setClose.addEventListener('click', closeSettings);
@@ -4588,7 +4769,7 @@ async function init() {
4588
4769
  if ($('#botOverviewBack')) $('#botOverviewBack').addEventListener('click', () => openBotPage(null));
4589
4770
  if ($('#botChip')) $('#botChip').addEventListener('click', () => openBotPage(activeBotId));
4590
4771
  /* kalıcı aktif bot: restart sonrası aynı botun UI'ı açılır */
4591
- beast.botsActiveGet().then((r) => { activeBotId = (r && r.id) || 'beast'; updateBotChip(); renderBotCards(); }).catch(() => {});
4772
+ beast.botsActiveGet().then((r) => { activeBotId = (r && r.id) || 'beast'; updateBotChip(); renderBotCards(); applyState(); }).catch(() => {});
4592
4773
  document.querySelectorAll('.btab').forEach((b) =>
4593
4774
  b.addEventListener('click', () => {
4594
4775
  document.querySelectorAll('.btab').forEach((x) => x.classList.remove('active'));
@@ -5101,7 +5282,7 @@ function ghCard(repo, isFav) {
5101
5282
  </div>
5102
5283
  ${repo.description ? `<div class="gh-desc" title="${escapeHtml(repo.description)}">${escapeHtml(repo.description)}</div>` : ''}
5103
5284
  <div class="gh-actions">
5104
- <button class="gh-fav ${isFav ? 'on' : ''}" data-act="fav">${isFav ? '★ Favoride' : '☆ Favorile'}</button>
5285
+ <button class="gh-fav ${isFav ? 'on' : ''}" data-act="fav">${isFav ? '★ Favori' : '☆ Favorile'}</button>
5105
5286
  <button data-act="chat"><svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:4px"><path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4z"/></svg>Chate at</button>
5106
5287
  <button data-act="copy">⧉ Kopyala</button>
5107
5288
  </div>
@@ -5191,7 +5372,7 @@ $('#ghSearch').addEventListener('keydown', (e) => {
5191
5372
  updateGhSub();
5192
5373
  loadGhTrend();
5193
5374
  });
5194
- /* sol sekme: Trend Depolar ⇄ Tüm Repolar (eşit bölünmüş, ortalanmış) */
5375
+ /* sol sekme: Trend Repolar ⇄ Tüm Repolar (eşit bölünmüş, ortalanmış) */
5195
5376
  document.querySelectorAll('.gh-tab').forEach((b) =>
5196
5377
  b.addEventListener('click', () => {
5197
5378
  ghState.ghTab = b.dataset.ghtab;