beast-agent 0.25.6 → 0.25.8

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,16 +471,21 @@ 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
+ BOT BAĞLAMA MİGRASYONU: bot kaydı olmayan (eski) oturumlar HER botun
476
+ listesinde de görünür — kullanıcı botunda açıp ilk mesajı yazınca
477
+ oturum KALICI olarak o bota bağlanır (agent:send auto-bind; notlar dahil).
478
+ Başka bir bota AÇIKÇA bağlı olanlar yalnız o botun listesinde. */
475
479
  for (const s of list) {
476
- if ((s.botId || 'beast') !== activeBotId) continue;
480
+ if (s.botId && s.botId !== activeBotId) continue;
481
+ const unbound = !s.botId && activeBotId !== 'beast';
477
482
  const row = document.createElement('div');
478
483
  row.className = 'sess' + (s.id === activeId ? ' active' : '');
479
484
  row.innerHTML =
480
485
  (waSet.has(s.id) ? '<span class="sess-wa" title="WhatsApp">W</span>' : '') +
481
486
  (tgSet.has(s.id) ? '<span class="sess-tg" title="Telegram">T</span>' : '') +
482
487
  `<span class="sess-title">${escapeHtml(s.title || 'Yeni Sohbet')}</span>` +
483
- `<span class="sess-code" title="Oturum kodu">${escapeHtml(s.code || '')}</span>` +
488
+ `<span class="sess-code" title="${unbound ? 'bağlantı bekliyor — ilk mesajda bu bota bağlanır' : 'Oturum kodu'}">${escapeHtml(s.code || '')}${unbound ? ' ·?' : ''}</span>` +
484
489
  `<button class="sess-del" title="Sil">×</button>`;
485
490
  row.addEventListener('click', () => openSession(s.id));
486
491
  row.querySelector('.sess-del').addEventListener('click', async (e) => {
@@ -815,7 +820,7 @@ async function renderProviderPane() {
815
820
 
816
821
  for (const m of state.models || []) {
817
822
  const row = document.createElement('div');
818
- row.className = 'prov-row' + (state.activeModel && state.activeModel.sel === m.sel ? ' active' : '');
823
+ row.className = 'prov-row' + (effectiveModelSel() === m.sel ? ' active' : '');
819
824
  row.innerHTML =
820
825
  `<span class="prov-name">${escapeHtml(m.providerName)}</span>` +
821
826
  `<span class="prov-model">${escapeHtml(m.model)}</span>` +
@@ -823,6 +828,7 @@ async function renderProviderPane() {
823
828
  `<button class="prov-del" title="${_t('p_del_title')}">✕</button>`;
824
829
  row.addEventListener('click', async () => {
825
830
  state = await beast.setModel(m.sel);
831
+ await refreshBots().catch(() => {});
826
832
  applyState();
827
833
  renderProviderPane();
828
834
  toast(_t('p_model_toast') + m.providerName + ' · ' + m.model);
@@ -1535,6 +1541,35 @@ async function renderIntegrationsPane() {
1535
1541
  <button id="tgAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1536
1542
  </div>
1537
1543
  <div class="sub" style="margin-top:8px">${_t('it_tg_note')}</div>
1544
+ </div>
1545
+
1546
+ <div class="wa-card" style="margin-top:14px">
1547
+ <div class="wa-head">
1548
+ <div class="wa-logo tg">D</div>
1549
+ <div>
1550
+ <div class="wa-title">Discord</div>
1551
+ <div class="wa-sub">${_t('it_dc_sub')}</div>
1552
+ </div>
1553
+ </div>
1554
+ <div class="wa-status"><span id="dcDot" class="wa-dot"></span><span id="dcStatText">—</span></div>
1555
+ <div id="dcUser" class="wa-user" hidden></div>
1556
+ <div class="form-grid" style="grid-template-columns:1fr auto;align-items:center">
1557
+ <input id="dcTokenInp" class="inp" type="password" style="margin:6px 0 0" placeholder="${_t('it_dc_token_ph')}" autocomplete="off" />
1558
+ <button id="dcSaveBtn" class="btn" style="margin-top:6px">${_t('it_tg_save')}</button>
1559
+ </div>
1560
+ <div class="wa-actions" style="margin-top:6px">
1561
+ <button id="dcStopBtn" class="btn ghost">${_t('it_disconnect')}</button>
1562
+ </div>
1563
+ <div class="divider"></div>
1564
+ <label class="mem-label" style="margin-top:0">${_t('it_allow_label')} — Discord</label>
1565
+ <div id="dcAllowChips" class="chips-inline"></div>
1566
+ <div class="form-grid" style="grid-template-columns:1.2fr 1fr 1fr auto;align-items:center">
1567
+ <input id="dcAllowNameInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_name_ph')}" autocomplete="off" />
1568
+ <input id="dcAllowIdInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_dc_id_ph')}" autocomplete="off" />
1569
+ <select id="dcAllowBotSel" class="perm-select" style="margin:6px 0 0;min-width:105px" title="${_t('bot_bind_title')}"></select>
1570
+ <button id="dcAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1571
+ </div>
1572
+ <div class="sub" style="margin-top:8px">${_t('it_dc_note')}</div>
1538
1573
  </div>`;
1539
1574
 
1540
1575
  const waGroupsOn = $('#waGroupsOn');
@@ -1597,6 +1632,29 @@ async function renderIntegrationsPane() {
1597
1632
  tgUI.user = ts.user || null;
1598
1633
  updateTgPane();
1599
1634
  } catch {}
1635
+
1636
+ /* DISCORD: token + allow list + durum */
1637
+ $('#dcSaveBtn').addEventListener('click', async () => {
1638
+ const tok = $('#dcTokenInp').value.trim();
1639
+ if (!tok) { toast(_t('it_dc_token_ph')); return; }
1640
+ toast(_t('it_dc_connecting'));
1641
+ const r = await beast.dcSetToken(tok).catch((e) => ({ status: 'error', error: String(e) }));
1642
+ $('#dcTokenInp').value = '';
1643
+ updateDcPane();
1644
+ toast(r.status === 'connected' ? 'Discord bağlı: ' + (r.user || '') : r.status === 'error' ? _t('it_dc_token_bad') : _t('it_dc_connecting'));
1645
+ });
1646
+ $('#dcStopBtn').addEventListener('click', async () => {
1647
+ await beast.dcStop().catch(() => {});
1648
+ updateDcPane();
1649
+ toast('Kesildi');
1650
+ });
1651
+ await renderDcAllow();
1652
+ try {
1653
+ const ds = await beast.dcGetStatus();
1654
+ dcUI.status = ds.status || 'disconnected';
1655
+ dcUI.user = ds.user || null;
1656
+ updateDcPane();
1657
+ } catch {}
1600
1658
  }
1601
1659
 
1602
1660
  /* ---------------- Olay Merkezi (ayrı sekme) ---------------- */
@@ -2439,6 +2497,199 @@ async function renderTgAllow() {
2439
2497
  }
2440
2498
  }
2441
2499
 
2500
+ /* ---------------- Discord izin listesi + durum — TG ile aynı mantık ---------------- */
2501
+
2502
+ const dcUI = { status: 'disconnected', user: null };
2503
+ const DC_STATUS_TEXT = {
2504
+ disconnected: 'Bağlı değil',
2505
+ connecting: 'Bağlanıyor…',
2506
+ connected: 'Bağlı',
2507
+ error: 'Hata — tokenı kontrol et',
2508
+ };
2509
+
2510
+ function onDcEvent(ev) {
2511
+ if (ev.type !== 'status') return;
2512
+ dcUI.status = ev.status;
2513
+ if (ev.user) dcUI.user = ev.user;
2514
+ updateDcPane();
2515
+ }
2516
+
2517
+ function updateDcPane() {
2518
+ const pane = $('#tab-integrations');
2519
+ const dot = pane && pane.querySelector('#dcDot');
2520
+ if (!dot) return;
2521
+ dot.className = 'wa-dot' + (dcUI.status === 'connected' ? ' on' : dcUI.status === 'error' ? ' qr' : '');
2522
+ pane.querySelector('#dcStatText').textContent = DC_STATUS_TEXT[dcUI.status] || dcUI.status;
2523
+ const u = pane.querySelector('#dcUser');
2524
+ u.hidden = !(dcUI.status === 'connected' && dcUI.user);
2525
+ u.textContent = dcUI.user ? '🤖 ' + dcUI.user : '';
2526
+ }
2527
+
2528
+ async function renderDcAllow() {
2529
+ const wrap = $('#dcAllowChips');
2530
+ if (!wrap) return;
2531
+ const list = await beast.dcGetAllow();
2532
+ let botChoices = [];
2533
+ try { botChoices = (await beast.botsList()) || []; } catch {}
2534
+ const dcLabel = (e) => {
2535
+ if (e === '*') return '* herkes';
2536
+ if (typeof e === 'string') return e;
2537
+ const name = String((e && e.name) || '').trim();
2538
+ const id = String((e && e.id) || '');
2539
+ const bot = e && e.bot_id ? botChoices.find((bb) => bb.id === e.bot_id) : null;
2540
+ return (name ? name + ' ' : '') + id + (bot ? ' → ' + bot.name : '');
2541
+ };
2542
+ wrap.innerHTML = '';
2543
+ if (!list.length) wrap.innerHTML = '<span class="sub">— boş —</span>';
2544
+ list.forEach((entry, idx) => {
2545
+ const curPerm = (typeof entry === 'object' && entry.perm) || (entry && entry.lockdown ? 'chat' : 'all');
2546
+ const c = document.createElement('span');
2547
+ c.className = 'chip';
2548
+ c.style.margin = '0 6px 6px 0';
2549
+ const txt = document.createElement('span');
2550
+ txt.className = 'chip-txt';
2551
+ txt.textContent = dcLabel(entry);
2552
+ c.appendChild(txt);
2553
+ if (entry !== '*') {
2554
+ const selP = document.createElement('select');
2555
+ selP.className = 'perm-select';
2556
+ selP.title = _t('wa_perm_title');
2557
+ const PERMS = [
2558
+ ['all', _t('wa_perm_all')],
2559
+ ['web', 'web'],
2560
+ ['read', _t('wa_perm_read')],
2561
+ ['chat', _t('wa_perm_chat')],
2562
+ ];
2563
+ for (const [v, lbl] of PERMS) {
2564
+ const o = document.createElement('option');
2565
+ o.value = v;
2566
+ o.textContent = lbl;
2567
+ if (curPerm === v) o.selected = true;
2568
+ selP.appendChild(o);
2569
+ }
2570
+ selP.addEventListener('change', async () => {
2571
+ const cur = await beast.dcGetAllow();
2572
+ const next = cur.map((e, i) => {
2573
+ if (i !== idx) return e;
2574
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2575
+ base.lockdown = selP.value === 'chat';
2576
+ base.perm = selP.value;
2577
+ return base;
2578
+ });
2579
+ await beast.dcSetAllow(next);
2580
+ renderDcAllow();
2581
+ toast(dcLabel(entry) + ': ' + selP.selectedOptions[0].textContent);
2582
+ });
2583
+ c.appendChild(selP);
2584
+
2585
+ if (botChoices.length) {
2586
+ const sel = document.createElement('select');
2587
+ sel.className = 'perm-select';
2588
+ sel.title = _t('bot_bind_title');
2589
+ const empty = document.createElement('option');
2590
+ empty.value = '';
2591
+ empty.textContent = _t('bot_sel_empty');
2592
+ sel.appendChild(empty);
2593
+ for (const bb of botChoices) {
2594
+ const o = document.createElement('option');
2595
+ o.value = bb.id;
2596
+ o.textContent = `${bb.icon} ${bb.name}`;
2597
+ if ((typeof entry === 'object' && entry.bot_id) === bb.id) o.selected = true;
2598
+ sel.appendChild(o);
2599
+ }
2600
+ sel.addEventListener('change', async () => {
2601
+ const cur = await beast.dcGetAllow();
2602
+ const next = cur.map((e, i) => {
2603
+ if (i !== idx) return e;
2604
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2605
+ base.bot_id = sel.value || undefined;
2606
+ return base;
2607
+ });
2608
+ await beast.dcSetAllow(next);
2609
+ renderDcAllow();
2610
+ const bt = botChoices.find((bb) => bb.id === sel.value);
2611
+ toast(dcLabel(entry) + ' → ' + (bt ? bt.name : '?'));
2612
+ });
2613
+ c.appendChild(sel);
2614
+ }
2615
+
2616
+ const isOwnerEntry = typeof entry === 'object' && !!entry.owner;
2617
+ const ownerBtn = document.createElement('span');
2618
+ ownerBtn.className = 'lk';
2619
+ ownerBtn.style.cssText = 'font-size:11px;padding:1px 6px;' + (isOwnerEntry ? 'color:#c9a227;font-weight:800' : '');
2620
+ ownerBtn.title = isOwnerEntry ? _t('wa_owner_title_on') : _t('wa_owner_title_off');
2621
+ ownerBtn.textContent = isOwnerEntry ? _t('wa_owner_on') : _t('wa_owner_off');
2622
+ ownerBtn.addEventListener('click', async () => {
2623
+ if (isOwnerEntry) return;
2624
+ const cur = await beast.dcGetAllow();
2625
+ const next = cur.map((e, i) => {
2626
+ if (typeof e === 'object' && e && e.owner) return { ...e, owner: false };
2627
+ if (i === idx) {
2628
+ const obj = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2629
+ obj.owner = true;
2630
+ return obj;
2631
+ }
2632
+ return e;
2633
+ });
2634
+ const tgt = next[idx];
2635
+ await beast.dcSetAllow(next);
2636
+ renderDcAllow();
2637
+ toast('Sahip: ' + dcLabel(tgt));
2638
+ });
2639
+ c.appendChild(ownerBtn);
2640
+ }
2641
+ const x = document.createElement('span');
2642
+ x.className = 'x';
2643
+ x.textContent = '×';
2644
+ x.addEventListener('click', async () => {
2645
+ const next = (await beast.dcGetAllow()).filter((_v, i) => i !== idx);
2646
+ await beast.dcSetAllow(next);
2647
+ renderDcAllow();
2648
+ toast('Kaldırıldı: ' + dcLabel(entry));
2649
+ });
2650
+ c.appendChild(x);
2651
+ wrap.appendChild(c);
2652
+ });
2653
+
2654
+ const inp = $('#dcAllowIdInp');
2655
+ const nameInp = $('#dcAllowNameInp');
2656
+ const add = $('#dcAllowAdd');
2657
+ const botSel = $('#dcAllowBotSel');
2658
+ if (botSel) {
2659
+ const prev = botSel.value;
2660
+ botSel.innerHTML =
2661
+ `<option value="">${_t('bot_sel_empty')}</option>` +
2662
+ botChoices
2663
+ .map((bb) => `<option value="${bb.id}">${bb.icon} ${escapeHtml(bb.name)}${bb.admin ? ' (admin)' : ''}</option>`)
2664
+ .join('');
2665
+ if (prev) botSel.value = prev;
2666
+ }
2667
+ if (!add.dataset.bound) {
2668
+ add.dataset.bound = '1';
2669
+ const addEntry = async () => {
2670
+ let v = inp.value.trim();
2671
+ if (!v) return;
2672
+ const name = nameInp.value.trim().slice(0, 40);
2673
+ if (!name) { toast('İsim zorunlu — kimin yazdığını bilmek için'); nameInp.focus(); return; }
2674
+ v = v.startsWith('@') ? '@' + v.slice(1).replace(/[^\w]/g, '') : v.replace(/[^\d]/g, '');
2675
+ if (!v) { toast(_t('it_dc_id_ph')); return; }
2676
+ const botId = botSel && botSel.value ? { bot_id: botSel.value } : {};
2677
+ const next = [...(await beast.dcGetAllow()), { id: v, name, ...botId }];
2678
+ if (!next.some((e) => e && typeof e === 'object' && e.owner)) {
2679
+ if (next.length && typeof next[0] === 'object') next[0].owner = true;
2680
+ }
2681
+ await beast.dcSetAllow(next);
2682
+ inp.value = '';
2683
+ nameInp.value = '';
2684
+ renderDcAllow();
2685
+ toast('Eklendi: ' + name);
2686
+ };
2687
+ add.addEventListener('click', addEntry);
2688
+ inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2689
+ nameInp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2690
+ }
2691
+ }
2692
+
2442
2693
  /* ---------------- BOT SİSTEMİ (BÖLÜM 2-3-4) ----------------
2443
2694
  Sol panel altında bot kartları; her bot için sohbet geçmişi + sekmeli yönetim.
2444
2695
  İlk bot hep Beast (admin, silinemez). Max 5 bot. */
@@ -2474,6 +2725,8 @@ async function refreshBots() {
2474
2725
  /* BOT PICKER SENKRONU: liste her yenilendiğinde üstteki rozet de aktif bota
2475
2726
  ayarlanır — startup yarışında (liste boşken çizilen) eski rozet düzelir */
2476
2727
  updateBotChip();
2728
+ /* model picker da aktif botun kendi modelini göstersin */
2729
+ if (state) applyState();
2477
2730
  if (!$('#botOverlay').hidden) renderBotPage();
2478
2731
  }
2479
2732
 
@@ -2531,12 +2784,15 @@ async function switchBot(id) {
2531
2784
  }
2532
2785
  } catch {}
2533
2786
  try { localStorage.setItem('beast.activeBot', id); } catch {}
2534
- /* o botun en son oturumuna geç; hiç yoksa o bot için yeni sohbet aç */
2787
+ /* o botun en son oturumuna geç; hiç yoksa o bot için yeni sohbet aç.
2788
+ (bot kaydı olmayan eski oturumlar da sayılır — ilk mesajda bağlanır) */
2535
2789
  try {
2536
- const list = (await beast.listSessions()).filter((s) => (s.botId || 'beast') === id);
2790
+ const list = (await beast.listSessions()).filter((s) => !s.botId || s.botId === id);
2537
2791
  if (list.length) await openSession(list[0].id);
2538
2792
  else { const v = await beast.createSession(); await openSession(v.id); }
2539
2793
  } catch {}
2794
+ /* picker, aktif botun kendi modelini göstersin */
2795
+ applyState();
2540
2796
  toast((b.icon || '🤖') + ' ' + b.name + (b.admin ? '' : ' — ' + _t('bot_switched')));
2541
2797
  }
2542
2798
 
@@ -2638,7 +2894,7 @@ async function renderBotNotes(pane, b) {
2638
2894
  pane.innerHTML = `<h2>${_t('notes_h2')}</h2><div class="sub">${_t('notes_sub')}</div>`;
2639
2895
  let all = [];
2640
2896
  try { all = await beast.listNotes(); } catch {}
2641
- const list = (b.admin ? all : all.filter((n) => (n.botId || 'beast') === b.id))
2897
+ const list = (b.admin ? all : all.filter((n) => !n.botId || n.botId === b.id))
2642
2898
  .slice()
2643
2899
  .sort((x, y) => String(y.updatedAt).localeCompare(String(x.updatedAt)));
2644
2900
  if (!list.length) {
@@ -2699,7 +2955,8 @@ async function renderBotChats(b) {
2699
2955
  const sessions = [];
2700
2956
  try {
2701
2957
  for (const s of await beast.listSessions()) {
2702
- if ((s.botId || 'beast') === b.id) sessions.push(s);
2958
+ /* bot kaydı olmayan (eski) oturumlar da gösterilir — ilk mesajda bu bota bağlanır */
2959
+ if (!s.botId || s.botId === b.id) sessions.push(s);
2703
2960
  }
2704
2961
  } catch {}
2705
2962
  sessions.sort((x, y) => String(y.updatedAt).localeCompare(String(x.updatedAt)));
@@ -2767,6 +3024,15 @@ function renderBotSettings(pane, b) {
2767
3024
  const numRows = (b.numbers || [])
2768
3025
  .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>`)
2769
3026
  .join('');
3027
+ /* bot bazlı model seçenekleri: ana picker'daki listeyle aynı (state.models) */
3028
+ const modelOpts = ['<option value="">' + escapeHtml(_t('bot_model_global')) + '</option>']
3029
+ .concat(
3030
+ (state.models || []).map((m) => {
3031
+ const s = m.sel || (m.providerId || '') + '::' + (m.model || '');
3032
+ return `<option value="${escapeHtml(s)}" ${b.model === s ? 'selected' : ''}>${escapeHtml(m.providerName || m.providerId || '')} · ${escapeHtml(m.model || '')}</option>`;
3033
+ })
3034
+ )
3035
+ .join('');
2770
3036
  /* Numara ekleme KALDIRILDI — girişler yalnız Ayarlar → Entegrasyonlar (WhatsApp
2771
3037
  izin listesi) üzerinden yapılır. Bu bölüm salt-okunur bağlı-numara listesidir. */
2772
3038
  f.innerHTML = `
@@ -2789,7 +3055,10 @@ function renderBotSettings(pane, b) {
2789
3055
  <div class="bot-checks" id="bPerm">${permChecks}</div>
2790
3056
  <div class="sub" style="margin-top:4px">${_t('bot_perm_note')}</div>
2791
3057
  <label class="mem-label">${_t('bot_skills')}</label>
2792
- <div class="bot-checks" id="bSkills">${skillChecks}</div>`}
3058
+ <div class="bot-checks" id="bSkills">${skillChecks}</div>
3059
+ <label class="mem-label">${_t('bot_model')}</label>
3060
+ <select id="bModel" class="inp">${modelOpts}</select>
3061
+ <div class="sub" style="margin-top:4px">${_t('bot_model_note')}</div>`}
2793
3062
  <label class="mem-label">${_t('bot_browser')}</label>
2794
3063
  <div class="bot-checks" style="margin-bottom:6px">
2795
3064
  <label><input type="checkbox" id="bExtBrowser" ${b.extBrowser ? 'checked' : ''}/> ${_t('bot_ext_browser')}</label>
@@ -2864,6 +3133,8 @@ function renderBotSettings(pane, b) {
2864
3133
  const sk = {};
2865
3134
  f.querySelectorAll('#bSkills input').forEach((c) => { sk[c.dataset.skill] = c.checked; });
2866
3135
  patch.skills = sk;
3136
+ const bm = $('#bModel');
3137
+ if (bm) patch.model = bm.value;
2867
3138
  }
2868
3139
  const r = await beast.botsUpdate(b.id, patch);
2869
3140
  if (r.ok) { toast(_t('bot_saved')); await refreshBots(); renderBotPage(); }
@@ -2921,7 +3192,7 @@ async function renderBotWatcher(pane, b) {
2921
3192
  try { items = (await beast.watchersList()) || []; } catch {}
2922
3193
  let botSessions = new Set();
2923
3194
  try {
2924
- for (const s of await beast.listSessions()) if ((s.botId || 'beast') === b.id) botSessions.add(s.id);
3195
+ for (const s of await beast.listSessions()) if (!s.botId || s.botId === b.id) botSessions.add(s.id);
2925
3196
  } catch {}
2926
3197
  const mine = items.filter((w) => botSessions.has(w.sessionId));
2927
3198
  if (!mine.length) {
@@ -3087,6 +3358,39 @@ function fmtAgo(iso) {
3087
3358
  return s >= 60 ? Math.floor(s / 60) + 'dk' : s + 'sn';
3088
3359
  }
3089
3360
 
3361
+ /* canlı süre sayacı: 0:43 · 2:05 · 1:02:15 biçimi (koşan ajan kartlarında) */
3362
+ function fmtElapsed(ms) {
3363
+ const s = Math.max(0, Math.floor(ms / 1000));
3364
+ const h = Math.floor(s / 3600);
3365
+ const m = Math.floor((s % 3600) / 60);
3366
+ const sec = s % 60;
3367
+ const p = (n) => String(n).padStart(2, '0');
3368
+ return h > 0 ? `${h}:${p(m)}:${p(sec)}` : `${m}:${p(sec)}`;
3369
+ }
3370
+
3371
+ /* koşan ajanların ⏱ sayaçları — saniyede bir yalnızca sayaç span'leri güncellenir
3372
+ (tüm rail yeniden çizilmez). Koşan kart kalmayınca interval kapanır. */
3373
+ let agentTimerInt = null;
3374
+ function ensureAgentTimers() {
3375
+ const any = !!document.querySelector('.agent-timer[data-start]');
3376
+ if (any && !agentTimerInt) {
3377
+ agentTimerInt = setInterval(() => {
3378
+ document.querySelectorAll('.agent-timer[data-start]').forEach((el) => {
3379
+ const t0 = Date.parse(el.dataset.start || '');
3380
+ if (t0) el.textContent = fmtElapsed(Date.now() - t0);
3381
+ });
3382
+ }, 1000);
3383
+ } else if (!any && agentTimerInt) {
3384
+ clearInterval(agentTimerInt);
3385
+ agentTimerInt = null;
3386
+ }
3387
+ }
3388
+ function agentTimerHtml(startedAt) {
3389
+ const t0 = Date.parse(startedAt || '');
3390
+ const val = t0 ? fmtElapsed(Date.now() - t0) : '0:00';
3391
+ return `<span class="agent-timer" data-start="${escapeHtml(startedAt || '')}">⏱ ${val}</span>`;
3392
+ }
3393
+
3090
3394
  /* sağ panel — paralel ajanların arka plan işleri */
3091
3395
  function renderAgentRail() {
3092
3396
  const list = els.railList;
@@ -3117,7 +3421,7 @@ function renderAgentRail() {
3117
3421
  `<span class="ag-dot"></span>` +
3118
3422
  `<span class="sess-title">${escapeHtml(j.title)}</span>` +
3119
3423
  (j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
3120
- `<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>` +
3424
+ `<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>` +
3121
3425
  (j.status === 'running'
3122
3426
  ? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
3123
3427
  : `<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>`) +
@@ -3162,6 +3466,7 @@ function renderAgentRail() {
3162
3466
  /* açık sohbet dökümünü re-render sonrası GERİ GETİR (kapanma bug'ının fix'i) */
3163
3467
  if (open && agentState.chatOpen.has(j.id)) maybeLoadRailChat(j.id);
3164
3468
  }
3469
+ ensureAgentTimers();
3165
3470
  }
3166
3471
 
3167
3472
  /* re-render storm'da IPC fırtınası olmasın: döküm 2 sn'de bir tazelenir */
@@ -3247,7 +3552,7 @@ function renderAgentsPane() {
3247
3552
  `<span class="ag-dot"></span>` +
3248
3553
  `<span class="ag-title">${escapeHtml(j.title)}</span>` +
3249
3554
  `<span class="ag-st">${agStText(j.status) || j.status}</span>` +
3250
- `<span class="ag-time">${when}${j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
3555
+ `<span class="ag-time">${when}${j.status === 'running' ? ' · ' + agentTimerHtml(j.startedAt) : j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
3251
3556
  (j.status === 'running'
3252
3557
  ? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
3253
3558
  : ``) +
@@ -3285,16 +3590,8 @@ function renderAgentsPane() {
3285
3590
  list.appendChild(card);
3286
3591
  }
3287
3592
 
3288
- /* canlı süre sayacı */
3289
- clearInterval(agentState.tick);
3290
- agentState.tick = setInterval(() => {
3291
- if (setTab !== 'agents') { clearInterval(agentState.tick); return; }
3292
- for (const el of document.querySelectorAll('.agent-card.st-running .ag-time')) {
3293
- /* sadece saati yenile */
3294
- const t = el.textContent.split(' ')[0];
3295
- el.textContent = `${t} · ${_t('ag_working')}…`;
3296
- }
3297
- }, 5000);
3593
+ /* canlı süre sayacı — saniyede bir yalnız ⏱ sayaç span'leri güncellenir */
3594
+ ensureAgentTimers();
3298
3595
  }
3299
3596
 
3300
3597
  /* ---------------- events from engine ---------------- */
@@ -3738,15 +4035,26 @@ async function openCronModal() {
3738
4035
 
3739
4036
  /* ---------------- state / controls ---------------- */
3740
4037
 
4038
+ /* AKTİF BOTUN etkili model seçimi: müşteri botun kendi modeli varsa o,
4039
+ yoksa (ve admin bot daysa) global picker seçimi. */
4040
+ function effectiveModelSel() {
4041
+ const b = botsCache.find((x) => x.id === activeBotId);
4042
+ return b && !b.admin && b.model ? b.model : (state.activeModel && state.activeModel.sel) || '';
4043
+ }
4044
+
3741
4045
  function applyState() {
3742
4046
  if (!state) return;
3743
- els.modelBtnLabel.textContent = state.activeModel
3744
- ? `${state.activeModel.providerName} · ${state.activeModel.model}`
3745
- : 'Model seçilmedi';
4047
+ const botSel = activeBotId && botsCache.length ? effectiveModelSel() : (state.activeModel && state.activeModel.sel);
4048
+ const bm = botSel ? (state.models || []).find((x) => x.sel === botSel) : null;
4049
+ els.modelBtnLabel.textContent = bm
4050
+ ? `${bm.providerName} · ${bm.model}`
4051
+ : state.activeModel
4052
+ ? `${state.activeModel.providerName} · ${state.activeModel.model}`
4053
+ : 'Model seçilmedi';
3746
4054
  els.thinkBtnLabel.innerHTML =
3747
- '<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>';
4055
+ '<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>';
3748
4056
  renderModelMenu();
3749
- }
4057
+ }
3750
4058
 
3751
4059
  /* düşünme (reasoning) seviyesi picker — gerçek API değerleri */
3752
4060
  const THINK_UI_LEVELS = [
@@ -3803,7 +4111,7 @@ function renderModelMenu() {
3803
4111
  els.modelList.innerHTML = '';
3804
4112
  for (const m of list) {
3805
4113
  const item = document.createElement('div');
3806
- item.className = 'dd-item' + (state.activeModel && state.activeModel.sel === m.sel ? ' active' : '');
4114
+ item.className = 'dd-item' + (effectiveModelSel() && effectiveModelSel() === m.sel ? ' active' : '');
3807
4115
  item.innerHTML =
3808
4116
  `<span class="dn">${escapeHtml(m.providerName)}</span>` +
3809
4117
  `<span class="dm">${escapeHtml(m.model)}</span>` +
@@ -3811,6 +4119,7 @@ function renderModelMenu() {
3811
4119
  item.addEventListener('click', async () => {
3812
4120
  state = await beast.setModel(m.sel);
3813
4121
  closeModelMenu();
4122
+ await refreshBots().catch(() => {}); // bot modeli değiştiyse botsCache tazelensin
3814
4123
  applyState();
3815
4124
  });
3816
4125
  els.modelList.appendChild(item);
@@ -4262,6 +4571,7 @@ async function init() {
4262
4571
 
4263
4572
  beast.onWaEvent(onWaEvent);
4264
4573
  beast.onTgEvent(onTgEvent);
4574
+ beast.onDcEvent(onDcEvent);
4265
4575
 
4266
4576
  els.gearBtn.addEventListener('click', openSettings);
4267
4577
  els.setClose.addEventListener('click', closeSettings);
@@ -4464,7 +4774,7 @@ async function init() {
4464
4774
  if ($('#botOverviewBack')) $('#botOverviewBack').addEventListener('click', () => openBotPage(null));
4465
4775
  if ($('#botChip')) $('#botChip').addEventListener('click', () => openBotPage(activeBotId));
4466
4776
  /* kalıcı aktif bot: restart sonrası aynı botun UI'ı açılır */
4467
- beast.botsActiveGet().then((r) => { activeBotId = (r && r.id) || 'beast'; updateBotChip(); renderBotCards(); }).catch(() => {});
4777
+ beast.botsActiveGet().then((r) => { activeBotId = (r && r.id) || 'beast'; updateBotChip(); renderBotCards(); applyState(); }).catch(() => {});
4468
4778
  document.querySelectorAll('.btab').forEach((b) =>
4469
4779
  b.addEventListener('click', () => {
4470
4780
  document.querySelectorAll('.btab').forEach((x) => x.classList.remove('active'));
@@ -121,18 +121,14 @@ function fakeJob(over) {
121
121
  };
122
122
  }
123
123
 
124
- test('superviseReason: takılan stuck, çalışan uzun iş once wrapup (wrapAt sonrası long), yeni uyarılan null', () => {
124
+ test('superviseReason: takılan stuck, uzun koşan iş CEO kontrolü (long), yeni uyarılan null', () => {
125
125
  assert.strictEqual(Engine.superviseReason(fakeJob(), Date.now()), 'stuck');
126
126
 
127
+ // süre sınırı KALDIRILDI: aktivite süren uzun iş artık kesilmez → CEO ara kontrolü (long)
127
128
  assert.strictEqual(
128
129
  Engine.superviseReason(fakeJob({ lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
129
- 'wrapup'
130
- ); // aktivite sürüyor ama 10 dk koştu — önce zarif bitirme uyarısı
131
-
132
- assert.strictEqual(
133
- Engine.superviseReason(fakeJob({ wrapAt: new Date().toISOString(), lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
134
130
  'long'
135
- ); // wrap-up uyarısı verildi, hâlâ bitmedi → CEO ara kontrolü
131
+ );
136
132
 
137
133
  assert.strictEqual(
138
134
  Engine.superviseReason(fakeJob({ startedAt: new Date(Date.now() - 1 * MIN).toISOString() }), Date.now()),
@@ -182,12 +178,11 @@ test('_supervise önce ajana ÖZ-KURTARMA verir, haklar bitince CEO\'ya uyarır'
182
178
  eng._supervise();
183
179
  assert.strictEqual(job.checks, 1);
184
180
 
185
- /* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (wrap-up zaten
186
- verildi, aktif) → CEO uyarısı ('long') */
181
+ /* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (aktivite sürüyor,
182
+ süre sınırı yok) → CEO uyarısı ('long') */
187
183
  job.status = 'running';
188
184
  job.endedAt = null;
189
185
  job.fixes = 2;
190
- job.wrapAt = new Date().toISOString();
191
186
  job.lastActivityAt = new Date(Date.now() - 30 * 1000).toISOString();
192
187
  job.lastNudgeAt = new Date(Date.now() - 5 * MIN).toISOString();
193
188
  eng._supervise();