beast-agent 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/preload.js CHANGED
@@ -115,6 +115,14 @@ contextBridge.exposeInMainWorld('beast', {
115
115
  waSetTts: (cfg) => ipcRenderer.invoke('wa:tts:set', cfg),
116
116
  waGetGroups: () => ipcRenderer.invoke('wa:groups:get'),
117
117
  waSetGroups: (cfg) => ipcRenderer.invoke('wa:groups:set', cfg),
118
+ tgGetStatus: () => ipcRenderer.invoke('tg:status:get'),
119
+ tgSetToken: (token) => ipcRenderer.invoke('tg:set', token),
120
+ tgStart: () => ipcRenderer.invoke('tg:start'),
121
+ tgStop: () => ipcRenderer.invoke('tg:stop'),
122
+ tgGetAllow: () => ipcRenderer.invoke('tg:allow:get'),
123
+ tgSetAllow: (list) => ipcRenderer.invoke('tg:allow:set', list),
124
+ tgListSessions: () => ipcRenderer.invoke('tg:sessions'),
125
+ onTgEvent: (cb) => ipcRenderer.on('tg:event', (_e, ev) => cb(ev)),
118
126
  getUsage: () => ipcRenderer.invoke('usage:get'),
119
127
  resetUsage: () => ipcRenderer.invoke('usage:reset'),
120
128
  createBackup: () => ipcRenderer.invoke('backup:create'),
@@ -73,7 +73,8 @@
73
73
  up_downloading: 'İndiriliyor…',
74
74
  up_downloaded: 'İndirildi — kurulum için hazır',
75
75
  up_npm_mode: 'npm kurulumu — güncellemeler otomatik kontrol edilir',
76
- up_npm_note: 'Yeni sürüm algılanınca buton aktifleşir: uygulama kapanır, güncellenir ve kendiliğinden yeniden açılır. Terminal komutu: beast-agent update',
76
+ up_npm_note: 'Yeni sürüm algılanınca buton aktifleşir: uygulama kapanır, güncellenir ve kendiliğinden yeniden açılır. Terminal komutu: beast update',
77
+ up_npm_started: 'Güncelleme başladı — uygulama kapanacak, en son sürüm kurulup kendiliğinden açılacak…',
77
78
  up_dev_note: 'Geliştirme modu — güncelleme buradan yapılmaz; dağıtım npm ve GitHub Releases üzerinden. Kaynak kod için: git pull',
78
79
  up_auto_check: 'Otomatik sürüm kontrolü (açılış + 6 saatte bir)',
79
80
  up_auto_dl: 'Yeni sürümü otomatik indir (kapanışta kurulur)',
@@ -199,6 +200,13 @@
199
200
  it_num_ph: 'Numara 905xxxxxxxxx',
200
201
  it_add: 'Ekle',
201
202
  it_allow_note: 'İsim + numara zorunlu. Her kişinin yanındaki serbest/web/okuma/kısıtlı seçimiyle kişiye özel yetki ver.',
203
+ it_tg_sub: 'Bot tokenı ile çalışır — izin listendeki kişilere Telegram\u2019dan cevap verir',
204
+ it_tg_token_ph: 'Bot tokenı (örn. 123456789:AAH…)',
205
+ it_tg_save: 'Kaydet & Bağlan',
206
+ it_tg_connecting: 'Telegram\u2019a bağlanıyor…',
207
+ it_tg_token_bad: 'Token doğrulanamadı — @BotFather\u2019dan aldığın tokenı kontrol et',
208
+ it_tg_id_ph: 'Kullanıcı ID veya @kullanıcı_adı',
209
+ it_tg_note: 'İzin listesi boşsa kimse cevap almaz. Bot oluşturma: @BotFather → /newbot. Kendi ID\u2019ni öğrenmek için @userinfobot\u2019a mesaj at.',
202
210
  ev_h2: 'Olay Merkezi',
203
211
  ev_sub: 'Cron\u2019suz canlı olaylar — kaynakları aç, abonelikleri agent kendisi kurar (event_subscribe). Tetiklenen olay ilgili sohbete düşer, cevap WhatsApp\u2019a gider.',
204
212
  ev_on: 'Olay merkezi açık',
@@ -374,6 +382,7 @@
374
382
  bot_tab_plugin: 'Plugin',
375
383
  bot_tab_watcher: 'Watcher',
376
384
  bot_tab_stats: 'İstatistik',
385
+ bot_tab_notes: 'Notlar',
377
386
  bot_name: 'Bot adı',
378
387
  bot_icon: 'İkon',
379
388
  bot_prompt: 'System prompt (kişilik/görev)',
@@ -396,6 +405,7 @@
396
405
  bot_see: 'Görebilir (diğer botlar)',
397
406
  bot_no_other: 'Başka bot yok.',
398
407
  bot_perm: 'Yetki seviyesi',
408
+ bot_perm_note: 'all tek başına tüm araçları açar (özel). Web / Okuma / Sohbet birden fazla seçilebilir — seçilenlerin araçları birleşir.',
399
409
  bot_skills: 'Skill erişimi',
400
410
  bot_browser: 'Tarayıcı ayarları',
401
411
  bot_ext_browser: 'Dış tarayıcı yetkisi (kullanıcı isterse)',
@@ -449,6 +459,12 @@
449
459
  p_zen_ok: '${n} free model yüklendi',
450
460
  p_zen_skipped: '${n} model yanıt vermedi, listeye alınmadı',
451
461
  p_zen_fail: 'Model kurulumu başarısız',
462
+ net_offline: 'İnternet bağlantısı yok — mesajların kuyruğa alındı',
463
+ net_online: 'Bağlantı geri geldi',
464
+ net_pill_offline: 'İnternet yok — ${n} mesaj kuyrukta',
465
+ net_pill_online: 'İnternet yok',
466
+ q_pending: 'kuyrukta — internet bekleniyor',
467
+ q_flushed: '${n} kuyruktaki mesaj gönderiliyor',
452
468
  bot_num_ro_hint: 'Numara ekleme/kişi adı Ayarlar → Entegrasyonlar (WhatsApp izin listesi) üzerinden yapılır; burada yalnızca bağlı numaralar görünür.',
453
469
  tipStore: 'Skills Store',
454
470
  tipIde: 'IDE Modu — dosyalar + chat + preview',
@@ -571,7 +587,8 @@
571
587
  up_downloading: 'Downloading…',
572
588
  up_downloaded: 'Downloaded — ready to install',
573
589
  up_npm_mode: 'npm install — updates are checked automatically',
574
- up_npm_note: 'The button activates when a new version is detected: the app closes, updates and relaunches by itself. Terminal command: beast-agent update',
590
+ up_npm_note: 'The button activates when a new version is detected: the app closes, updates and relaunches by itself. Terminal command: beast update',
591
+ up_npm_started: 'Update started — the app will close, install the latest version and relaunch…',
575
592
  up_dev_note: 'Development mode — updates are distributed via npm and GitHub Releases. For source code: git pull',
576
593
  up_auto_check: 'Automatic version check (on startup + every 6 hours)',
577
594
  up_auto_dl: 'Auto-download new versions (installed on quit)',
@@ -697,6 +714,13 @@
697
714
  it_num_ph: 'Number 905xxxxxxxxx',
698
715
  it_add: 'Add',
699
716
  it_allow_note: 'Name + number required. Grant per-person permission (free/web/read/restricted) via the selector next to each.',
717
+ it_tg_sub: 'Works with a bot token — replies on Telegram to people in your allow list',
718
+ it_tg_token_ph: 'Bot token (e.g. 123456789:AAH…)',
719
+ it_tg_save: 'Save & Connect',
720
+ it_tg_connecting: 'Connecting to Telegram…',
721
+ it_tg_token_bad: 'Token could not be verified — check the token from @BotFather',
722
+ it_tg_id_ph: 'User ID or @username',
723
+ it_tg_note: 'Empty allow list = nobody gets a reply. Create a bot: @BotFather → /newbot. To learn your ID, message @userinfobot.',
700
724
  ev_h2: 'Event Center',
701
725
  ev_sub: 'Live events without cron — open sources, the agent sets up subscriptions itself (event_subscribe). The triggered event drops into the relevant chat, the reply goes to WhatsApp.',
702
726
  ev_on: 'Event center on',
@@ -930,6 +954,7 @@
930
954
  bot_tab_plugin: 'Plugin',
931
955
  bot_tab_watcher: 'Watcher',
932
956
  bot_tab_stats: 'Stats',
957
+ bot_tab_notes: 'Notes',
933
958
  bot_name: 'Bot name',
934
959
  bot_icon: 'Icon',
935
960
  bot_prompt: 'System prompt (personality/task)',
@@ -952,6 +977,7 @@
952
977
  bot_see: 'Can see (other bots)',
953
978
  bot_no_other: 'No other bots.',
954
979
  bot_perm: 'Permission level',
980
+ bot_perm_note: 'all alone grants every tool (exclusive). Web / Read / Chat can be multi-selected — their tool sets are combined.',
955
981
  bot_skills: 'Skill access',
956
982
  bot_browser: 'Browser settings',
957
983
  bot_ext_browser: 'External browser permission (when user asks)',
@@ -1005,6 +1031,12 @@
1005
1031
  p_zen_ok: '${n} free models installed',
1006
1032
  p_zen_skipped: '${n} models did not respond, excluded',
1007
1033
  p_zen_fail: 'Model setup failed',
1034
+ net_offline: 'No internet connection — messages queued',
1035
+ net_online: 'Connection restored',
1036
+ net_pill_offline: 'No internet — ${n} message(s) queued',
1037
+ net_pill_online: 'No internet',
1038
+ q_pending: 'queued — waiting for internet',
1039
+ q_flushed: '${n} queued message(s) being sent',
1008
1040
  bot_num_ro_hint: 'Add numbers / person names via Settings → Integrations (WhatsApp allow list); only linked numbers are shown here.',
1009
1041
  tipStore: 'Skills Store',
1010
1042
  tipIde: 'IDE Mode — files + chat + preview',
@@ -240,6 +240,7 @@
240
240
  <button class="btab" data-btab="log" data-i18n="bot_tab_log">Log</button>
241
241
  <button class="btab" data-btab="watcher" data-i18n="bot_tab_watcher">Watcher</button>
242
242
  <button class="btab" data-btab="stats" data-i18n="bot_tab_stats">İstatistik</button>
243
+ <button class="btab" data-btab="notes" data-i18n="bot_tab_notes">Notlar</button>
243
244
  </nav>
244
245
  <div id="botPane"></div>
245
246
  </div>
@@ -193,6 +193,72 @@ function setStatus(text) {
193
193
  els.statusPill.textContent = text;
194
194
  }
195
195
 
196
+ /* ---------------- OFFLINE MESAJ KUYRUĞU (chat) ----------------
197
+ İnternet yokken gönderilen mesajlar main'deki diske yazılan kuyruğa düşer;
198
+ burada ⏳ "kuyrukta" balonu gösterilir. Bağlantı gelince kuyruk otomatik
199
+ boşaltılır ('netQueue flushed') ve balonlar gerçek mesajla değişir. */
200
+ let netOnline = true;
201
+ let netQueueCount = 0;
202
+ const netPending = []; // { key, el } — bekleyen mesaj balonları
203
+
204
+ function setNetBadge(el, text) {
205
+ if (!el || !el.isConnected) return;
206
+ let badge = el.querySelector('.q-badge');
207
+ if (!badge) {
208
+ badge = document.createElement('span');
209
+ badge.className = 'q-badge';
210
+ el.appendChild(badge);
211
+ }
212
+ badge.textContent = '⏳ ' + text;
213
+ }
214
+
215
+ function updateNetQueueUi() {
216
+ if (busy) return;
217
+ if (!netOnline) {
218
+ setStatus(netQueueCount > 0 ? _ti('net_pill_offline', netQueueCount) : _t('net_pill_online'));
219
+ } else if (!netQueueCount) {
220
+ setStatus('');
221
+ }
222
+ }
223
+
224
+ function onNetEvent(ev) {
225
+ if (ev.type === 'net') {
226
+ const was = netOnline;
227
+ netOnline = ev.online !== false;
228
+ if (was !== netOnline) {
229
+ if (netOnline) {
230
+ toast(_t('net_online'));
231
+ if (!netQueueCount) setStatus('');
232
+ } else {
233
+ toast(_t('net_offline'));
234
+ updateNetQueueUi();
235
+ }
236
+ }
237
+ return;
238
+ }
239
+ if (ev.type === 'netQueue') {
240
+ netQueueCount = typeof ev.count === 'number' ? ev.count : netQueueCount;
241
+ if (ev.queued) {
242
+ if (ev.sessionId === activeId) {
243
+ addUserBubble(ev.text || (ev.attCount ? `[${ev.attCount} ek]` : ''));
244
+ const el = els.msgs.querySelector('.msg-user:last-of-type');
245
+ if (el && ev.key) {
246
+ setNetBadge(el, _t('q_pending'));
247
+ netPending.push({ key: ev.key, el });
248
+ }
249
+ }
250
+ } else if (ev.flushed) {
251
+ /* kuyruk boşaltılıyor: ⏳ balonlarını kaldır — gerçek mesaj echo ile gelir */
252
+ for (const p of netPending) {
253
+ if (p.el && p.el.isConnected) p.el.remove();
254
+ }
255
+ netPending.length = 0;
256
+ toast(_ti('q_flushed', ev.flushed));
257
+ }
258
+ updateNetQueueUi();
259
+ }
260
+ }
261
+
196
262
  const TODO_GLYPH = { done: '✓', active: '▸', pending: '○' };
197
263
 
198
264
  function renderTodos(items) {
@@ -878,7 +944,7 @@ async function renderProviderPane() {
878
944
  form.innerHTML =
879
945
  /* TEK TİKLA MODEL (OpenCode Zen Free) */
880
946
  `<div class="zen-hero">` +
881
- `<button id="zenOneClick" class="btn">⚡ ${_t('p_zen_btn')}</button>` +
947
+ `<button id="zenOneClick" class="btn"><span class="zen-load" hidden></span><span class="zen-label">⚡ ${_t('p_zen_btn')}</span></button>` +
882
948
  `<div class="sub" style="margin-top:6px">${_t('p_zen_hint')}</div>` +
883
949
  `</div>` +
884
950
  `<label class="mem-label">${_t('p_preset')}</label>` +
@@ -928,13 +994,19 @@ async function renderProviderPane() {
928
994
  /* TEK TİKLA MODEL: OpenCode Zen free kurulumu */
929
995
  $('#zenOneClick').addEventListener('click', async () => {
930
996
  const btn = $('#zenOneClick');
931
- const old = btn.textContent;
997
+ const label = btn.querySelector('.zen-label');
998
+ const spinner = btn.querySelector('.zen-load');
999
+ const old = label.textContent;
932
1000
  btn.disabled = true;
933
- btn.textContent = _t('p_zen_busy');
1001
+ btn.classList.add('loading');
1002
+ spinner.hidden = false;
1003
+ label.textContent = _t('p_zen_busy');
934
1004
  let r;
935
1005
  try { r = await beast.zenOneClick(); } catch (e) { r = { ok: false, error: String((e && e.message) || e) }; }
936
1006
  btn.disabled = false;
937
- btn.textContent = old;
1007
+ btn.classList.remove('loading');
1008
+ spinner.hidden = true;
1009
+ label.textContent = old;
938
1010
  if (r && r.ok) {
939
1011
  let msg = _ti('p_zen_ok', (r.models || []).length) + ' (OpenCode Zen)';
940
1012
  if (r.failed && r.failed.length) msg += ' — ' + _ti('p_zen_skipped', r.failed.length);
@@ -1414,6 +1486,35 @@ async function renderIntegrationsPane() {
1414
1486
  <button id="waAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1415
1487
  </div>
1416
1488
  <div class="sub" style="margin-top:8px">${_t('it_allow_note')}</div>
1489
+ </div>
1490
+
1491
+ <div class="wa-card" style="margin-top:14px">
1492
+ <div class="wa-head">
1493
+ <div class="wa-logo tg">T</div>
1494
+ <div>
1495
+ <div class="wa-title">Telegram</div>
1496
+ <div class="wa-sub">${_t('it_tg_sub')}</div>
1497
+ </div>
1498
+ </div>
1499
+ <div class="wa-status"><span id="tgDot" class="wa-dot"></span><span id="tgStatText">—</span></div>
1500
+ <div id="tgUser" class="wa-user" hidden></div>
1501
+ <div class="form-grid" style="grid-template-columns:1fr auto;align-items:center">
1502
+ <input id="tgTokenInp" class="inp" type="password" style="margin:6px 0 0" placeholder="${_t('it_tg_token_ph')}" autocomplete="off" />
1503
+ <button id="tgSaveBtn" class="btn" style="margin-top:6px">${_t('it_tg_save')}</button>
1504
+ </div>
1505
+ <div class="wa-actions" style="margin-top:6px">
1506
+ <button id="tgStopBtn" class="btn ghost">${_t('it_disconnect')}</button>
1507
+ </div>
1508
+ <div class="divider"></div>
1509
+ <label class="mem-label" style="margin-top:0">${_t('it_allow_label')} — Telegram</label>
1510
+ <div id="tgAllowChips" class="chips-inline"></div>
1511
+ <div class="form-grid" style="grid-template-columns:1.2fr 1fr 1fr auto;align-items:center">
1512
+ <input id="tgAllowNameInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_name_ph')}" autocomplete="off" />
1513
+ <input id="tgAllowIdInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_tg_id_ph')}" autocomplete="off" />
1514
+ <select id="tgAllowBotSel" class="perm-select" style="margin:6px 0 0;min-width:105px" title="${_t('bot_bind_title')}"></select>
1515
+ <button id="tgAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1516
+ </div>
1517
+ <div class="sub" style="margin-top:8px">${_t('it_tg_note')}</div>
1417
1518
  </div>`;
1418
1519
 
1419
1520
  const waGroupsOn = $('#waGroupsOn');
@@ -1453,6 +1554,29 @@ async function renderIntegrationsPane() {
1453
1554
  waUI.status = snap.status || 'disconnected';
1454
1555
  if (snap.user) waUI.user = snap.user;
1455
1556
  updateWaPane();
1557
+
1558
+ /* TELEGRAM (FEATURE 3): token + allow list + durum */
1559
+ $('#tgSaveBtn').addEventListener('click', async () => {
1560
+ const tok = $('#tgTokenInp').value.trim();
1561
+ if (!tok) { toast(_t('it_tg_token_ph')); return; }
1562
+ toast(_t('it_tg_connecting'));
1563
+ const r = await beast.tgSetToken(tok).catch((e) => ({ status: 'error', error: String(e) }));
1564
+ $('#tgTokenInp').value = '';
1565
+ updateTgPane();
1566
+ toast(r.status === 'connected' ? 'Telegram bağlı: ' + (r.user || '') : r.status === 'error' ? _t('it_tg_token_bad') : _t('it_tg_connecting'));
1567
+ });
1568
+ $('#tgStopBtn').addEventListener('click', async () => {
1569
+ await beast.tgStop().catch(() => {});
1570
+ updateTgPane();
1571
+ toast('Kesildi');
1572
+ });
1573
+ await renderTgAllow();
1574
+ try {
1575
+ const ts = await beast.tgGetStatus();
1576
+ tgUI.status = ts.status || 'disconnected';
1577
+ tgUI.user = ts.user || null;
1578
+ updateTgPane();
1579
+ } catch {}
1456
1580
  }
1457
1581
 
1458
1582
  /* ---------------- Olay Merkezi (ayrı sekme) ---------------- */
@@ -1719,7 +1843,8 @@ async function renderUpdatePane(autoCheck) {
1719
1843
  const btnInstall = pane.querySelector('#upInstall');
1720
1844
  if (btnInstall) btnInstall.addEventListener('click', async () => {
1721
1845
  const r = await beast.updateInstall().catch(() => ({ ok: false, error: 'ipc' }));
1722
- if (!r.ok && r.error) toast(r.error);
1846
+ if (r.ok && r.npm) toast(_t('up_npm_started'));
1847
+ else if (!r.ok && r.error) toast(r.error);
1723
1848
  });
1724
1849
 
1725
1850
  /* indirme ilerlemesi için sekme açıkken canlı tazele — YALNIZ durum kutusu */
@@ -2085,6 +2210,172 @@ function updateWaPane() {
2085
2210
  u.textContent = waUI.user ? '👤 ' + waUI.user : '';
2086
2211
  }
2087
2212
 
2213
+ /* ---------------- integrations (Telegram — FEATURE 3) ---------------- */
2214
+
2215
+ const tgUI = { status: 'disconnected', user: null };
2216
+ const TG_STATUS_TEXT = {
2217
+ disconnected: 'Bağlı değil',
2218
+ connecting: 'Bağlanıyor…',
2219
+ connected: 'Bağlı',
2220
+ error: 'Hata — tokenı kontrol et',
2221
+ };
2222
+
2223
+ function onTgEvent(ev) {
2224
+ if (ev.type !== 'status') return;
2225
+ tgUI.status = ev.status;
2226
+ if (ev.user) tgUI.user = ev.user;
2227
+ updateTgPane();
2228
+ }
2229
+
2230
+ function updateTgPane() {
2231
+ const pane = $('#tab-integrations');
2232
+ const dot = pane && pane.querySelector('#tgDot');
2233
+ if (!dot) return;
2234
+ dot.className = 'wa-dot' + (tgUI.status === 'connected' ? ' on' : tgUI.status === 'error' ? ' qr' : '');
2235
+ pane.querySelector('#tgStatText').textContent = TG_STATUS_TEXT[tgUI.status] || tgUI.status;
2236
+ const u = pane.querySelector('#tgUser');
2237
+ u.hidden = !(tgUI.status === 'connected' && tgUI.user);
2238
+ u.textContent = tgUI.user ? '🤖 ' + tgUI.user : '';
2239
+ }
2240
+
2241
+ /* Telegram izin listesi — WA ile aynı mantık: isim zorunlu, kişi bazlı izin,
2242
+ bot eşleme. Chip × ile silinir; düzenlemek için silip yeniden ekle. */
2243
+ async function renderTgAllow() {
2244
+ const wrap = $('#tgAllowChips');
2245
+ if (!wrap) return;
2246
+ const list = await beast.tgGetAllow();
2247
+ let botChoices = [];
2248
+ try { botChoices = (await beast.botsList()) || []; } catch {}
2249
+ const multiBot = botChoices.length >= 2;
2250
+ const tgLabel = (e) => {
2251
+ if (e === '*') return '* herkes';
2252
+ if (typeof e === 'string') return e;
2253
+ const name = String((e && e.name) || '').trim();
2254
+ const id = String((e && e.id) || '');
2255
+ return (name ? name + ' ' : '') + id;
2256
+ };
2257
+ wrap.innerHTML = '';
2258
+ if (!list.length) wrap.innerHTML = '<span class="sub">— boş —</span>';
2259
+ list.forEach((entry, idx) => {
2260
+ const curPerm = (typeof entry === 'object' && entry.perm) || (entry && entry.lockdown ? 'chat' : 'all');
2261
+ const c = document.createElement('span');
2262
+ c.className = 'chip';
2263
+ c.style.margin = '0 6px 6px 0';
2264
+ const txt = document.createElement('span');
2265
+ txt.className = 'chip-txt';
2266
+ txt.textContent = tgLabel(entry);
2267
+ c.appendChild(txt);
2268
+ if (entry !== '*') {
2269
+ /* kişi bazlı granül izin: serbest/web/okuma/kısıtlı (WA ile aynı) */
2270
+ const selP = document.createElement('select');
2271
+ selP.className = 'perm-select';
2272
+ selP.title = _t('wa_perm_title');
2273
+ const PERMS = [
2274
+ ['all', _t('wa_perm_all')],
2275
+ ['web', 'web'],
2276
+ ['read', _t('wa_perm_read')],
2277
+ ['chat', _t('wa_perm_chat')],
2278
+ ];
2279
+ for (const [v, lbl] of PERMS) {
2280
+ const o = document.createElement('option');
2281
+ o.value = v;
2282
+ o.textContent = lbl;
2283
+ if (curPerm === v) o.selected = true;
2284
+ selP.appendChild(o);
2285
+ }
2286
+ selP.addEventListener('change', async () => {
2287
+ const cur = await beast.tgGetAllow();
2288
+ const next = cur.map((e, i) => {
2289
+ if (i !== idx) return e;
2290
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2291
+ base.lockdown = selP.value === 'chat';
2292
+ base.perm = selP.value;
2293
+ return base;
2294
+ });
2295
+ await beast.tgSetAllow(next);
2296
+ renderTgAllow();
2297
+ toast(tgLabel(entry) + ': ' + selP.selectedOptions[0].textContent);
2298
+ });
2299
+ c.appendChild(selP);
2300
+
2301
+ /* BOT SİSTEMİ: 2+ bot varsa kaydı hangi botun karşılayacağını seç */
2302
+ if (multiBot) {
2303
+ const sel = document.createElement('select');
2304
+ sel.className = 'perm-select';
2305
+ sel.title = _t('bot_bind_title');
2306
+ for (const bb of botChoices) {
2307
+ const o = document.createElement('option');
2308
+ o.value = bb.id;
2309
+ o.textContent = `${bb.icon} ${bb.name}`;
2310
+ if ((typeof entry === 'object' && entry.bot_id) === bb.id) o.selected = true;
2311
+ sel.appendChild(o);
2312
+ }
2313
+ sel.addEventListener('change', async () => {
2314
+ const cur = await beast.tgGetAllow();
2315
+ const next = cur.map((e, i) => {
2316
+ if (i !== idx) return e;
2317
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2318
+ base.bot_id = sel.value || undefined;
2319
+ return base;
2320
+ });
2321
+ await beast.tgSetAllow(next);
2322
+ renderTgAllow();
2323
+ const bt = botChoices.find((bb) => bb.id === sel.value);
2324
+ toast(tgLabel(entry) + ' → ' + (bt ? bt.name : '?'));
2325
+ });
2326
+ c.appendChild(sel);
2327
+ }
2328
+ }
2329
+ const x = document.createElement('span');
2330
+ x.className = 'x';
2331
+ x.textContent = '×';
2332
+ x.addEventListener('click', async () => {
2333
+ const next = (await beast.tgGetAllow()).filter((_v, i) => i !== idx);
2334
+ await beast.tgSetAllow(next);
2335
+ renderTgAllow();
2336
+ toast('Kaldırıldı: ' + tgLabel(entry));
2337
+ });
2338
+ c.appendChild(x);
2339
+ wrap.appendChild(c);
2340
+ });
2341
+
2342
+ /* ekleme formu: isim + (ID veya @username) + bot seçici */
2343
+ const inp = $('#tgAllowIdInp');
2344
+ const nameInp = $('#tgAllowNameInp');
2345
+ const add = $('#tgAllowAdd');
2346
+ const botSel = $('#tgAllowBotSel');
2347
+ if (botSel) {
2348
+ const prev = botSel.value;
2349
+ botSel.innerHTML =
2350
+ `<option value="">${_t('bot_sel_empty')}</option>` +
2351
+ botChoices
2352
+ .map((bb) => `<option value="${bb.id}">${bb.icon} ${escapeHtml(bb.name)}${bb.admin ? ' (admin)' : ''}</option>`)
2353
+ .join('');
2354
+ if (prev) botSel.value = prev;
2355
+ }
2356
+ if (!add.dataset.bound) {
2357
+ add.dataset.bound = '1';
2358
+ const addEntry = async () => {
2359
+ let v = inp.value.trim();
2360
+ if (!v) return;
2361
+ const name = nameInp.value.trim().slice(0, 40);
2362
+ if (!name) { toast('İsim zorunlu — kimin yazdığını bilmek için'); nameInp.focus(); return; }
2363
+ /* @username olduğu gibi; sayısal ID'den boşluk/nokta temizle */
2364
+ v = v.startsWith('@') ? '@' + v.slice(1).replace(/[^\w]/g, '') : v.replace(/[^\d]/g, '');
2365
+ if (!v) { toast(_t('it_tg_id_ph')); return; }
2366
+ const botId = botSel && botSel.value ? { bot_id: botSel.value } : {};
2367
+ await beast.tgSetAllow([...(await beast.tgGetAllow()), { id: v, name, ...botId }]);
2368
+ inp.value = '';
2369
+ nameInp.value = '';
2370
+ renderTgAllow();
2371
+ toast('Eklendi: ' + name);
2372
+ };
2373
+ add.addEventListener('click', addEntry);
2374
+ inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2375
+ nameInp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2376
+ }
2377
+ }
2378
+
2088
2379
  /* ---------------- BOT SİSTEMİ (BÖLÜM 2-3-4) ----------------
2089
2380
  Sol panel altında bot kartları; her bot için sohbet geçmişi + sekmeli yönetim.
2090
2381
  İlk bot hep Beast (admin, silinemez). Max 5 bot. */
@@ -2117,6 +2408,9 @@ async function refreshBots() {
2117
2408
  } catch { botsCache = []; }
2118
2409
  try { botPageStats = (await beast.botsStats()) || []; } catch { botPageStats = []; }
2119
2410
  renderBotCards();
2411
+ /* BOT PICKER SENKRONU: liste her yenilendiğinde üstteki rozet de aktif bota
2412
+ ayarlanır — startup yarışında (liste boşken çizilen) eski rozet düzelir */
2413
+ updateBotChip();
2120
2414
  if (!$('#botOverlay').hidden) renderBotPage();
2121
2415
  }
2122
2416
 
@@ -2159,11 +2453,20 @@ async function switchBot(id) {
2159
2453
  const b = botsCache.find((x) => x.id === id);
2160
2454
  if (!b) return;
2161
2455
  if (id === activeBotId) return;
2456
+ /* UI hemen dönsün: rozet + kartlar IPC beklemeden aktif bota geçer */
2162
2457
  activeBotId = id;
2163
- try { await beast.botsActivate(id); } catch {}
2164
- try { localStorage.setItem('beast.activeBot', id); } catch {}
2165
2458
  updateBotChip();
2166
2459
  renderBotCards();
2460
+ try {
2461
+ const r = await beast.botsActivate(id);
2462
+ /* main'inkiyle eşleş: normalizasyon ('beast' fallback) varsa düzelt */
2463
+ if (r && r.activeBotId && r.activeBotId !== activeBotId) {
2464
+ activeBotId = r.activeBotId;
2465
+ updateBotChip();
2466
+ renderBotCards();
2467
+ }
2468
+ } catch {}
2469
+ try { localStorage.setItem('beast.activeBot', id); } catch {}
2167
2470
  /* o botun en son oturumuna geç; hiç yoksa o bot için yeni sohbet aç */
2168
2471
  try {
2169
2472
  const list = (await beast.listSessions()).filter((s) => (s.botId || 'beast') === id);
@@ -2224,9 +2527,44 @@ function renderBotPage() {
2224
2527
  else if (tab === 'log') renderBotLog(pane, b);
2225
2528
  else if (tab === 'watcher') renderBotWatcher(pane, b);
2226
2529
  else if (tab === 'stats') renderBotStats(pane, b);
2530
+ else if (tab === 'notes') renderBotNotes(pane, b);
2227
2531
  renderBotChats(b);
2228
2532
  }
2229
2533
 
2534
+ /* --- Notlar sekmesi: oturum notları — konuşma kodu, başlık, tarih ve özet metni.
2535
+ Admin bot (beast) tüm oturumları görür; müşteri botu yalnız kendi oturumlarını. --- */
2536
+ async function renderBotNotes(pane, b) {
2537
+ pane.innerHTML = `<h2>${_t('notes_h2')}</h2><div class="sub">${_t('notes_sub')}</div>`;
2538
+ let all = [];
2539
+ try { all = await beast.listNotes(); } catch {}
2540
+ const list = (b.admin ? all : all.filter((n) => (n.botId || 'beast') === b.id))
2541
+ .slice()
2542
+ .sort((x, y) => String(y.updatedAt).localeCompare(String(x.updatedAt)));
2543
+ if (!list.length) {
2544
+ pane.insertAdjacentHTML('beforeend', `<div class="mini-empty">${_t('notes_empty')}</div>`);
2545
+ return;
2546
+ }
2547
+ const f = document.createElement('div');
2548
+ f.className = 'bot-form';
2549
+ for (const n of list) {
2550
+ const when = n.updatedAt ? new Date(n.updatedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
2551
+ const card = document.createElement('div');
2552
+ card.className = 'notes-card';
2553
+ card.innerHTML =
2554
+ `<div class="notes-head"><div class="skill-name">${escapeHtml((b.icon || '') + ' ' + (n.code || '?'))} — ${escapeHtml(n.title || '')}</div>` +
2555
+ `<div class="notes-meta">${escapeHtml(when)} · ${n.count || 0} ${_t('bot_stat_msgs')}</div></div>` +
2556
+ `<div class="notes-body">${escapeHtml(String(n.notes || '').slice(0, 4000))}</div>` +
2557
+ `<div style="margin-top:8px"><button class="btn ghost notes-del">${_t('notes_del')}</button></div>`;
2558
+ card.querySelector('.notes-del').addEventListener('click', async () => {
2559
+ await beast.clearNotes(n.id).catch(() => {});
2560
+ toast(_t('deleted'));
2561
+ renderBotNotes(pane, b);
2562
+ });
2563
+ f.appendChild(card);
2564
+ }
2565
+ pane.appendChild(f);
2566
+ }
2567
+
2230
2568
  function renderBotOverview(pane) {
2231
2569
  pane.innerHTML = `<h2>${_t('bot_overview')}</h2><div class="sub">${_t('bot_overview_sub')}</div>`;
2232
2570
  const totals = { numbers: 0, sessions: 0, msgs: 0 };
@@ -2313,6 +2651,12 @@ function renderBotSettings(pane, b) {
2313
2651
  const skillChecks = BOT_SKILLS
2314
2652
  .map(([k, lbl]) => `<label><input type="checkbox" data-skill="${k}" ${(b.skills || {})[k] ? 'checked' : ''}/> ${lbl}</label>`)
2315
2653
  .join('');
2654
+ /* Yetki: 'all' tek başına tüm araçları verir (özel); web/read/chat çoklu seçilebilir */
2655
+ const PERM_OPTS = [['all', _t('wa_perm_all')], ['web', 'Web'], ['read', _t('wa_perm_read')], ['chat', _t('wa_perm_chat')]];
2656
+ const curPerms = Array.isArray(b.perm) ? b.perm : [b.perm || 'all'];
2657
+ const permChecks = PERM_OPTS
2658
+ .map(([k, lbl]) => `<label><input type="checkbox" data-perm="${k}" ${curPerms.includes(k) ? 'checked' : ''}/> ${lbl}</label>`)
2659
+ .join('');
2316
2660
  const numRows = (b.numbers || [])
2317
2661
  .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>`)
2318
2662
  .join('');
@@ -2334,12 +2678,8 @@ function renderBotSettings(pane, b) {
2334
2678
  <div class="bot-checks" id="bSee">${seeChecks}</div>
2335
2679
  ${b.admin ? '' : `
2336
2680
  <label class="mem-label">${_t('bot_perm')}</label>
2337
- <select id="bPerm" class="inp">
2338
- <option value="all" ${b.perm === 'all' ? 'selected' : ''}>${_t('wa_perm_all')}</option>
2339
- <option value="web" ${b.perm === 'web' ? 'selected' : ''}>web</option>
2340
- <option value="read" ${b.perm === 'read' ? 'selected' : ''}>${_t('wa_perm_read')}</option>
2341
- <option value="chat" ${b.perm === 'chat' ? 'selected' : ''}>${_t('wa_perm_chat')}</option>
2342
- </select>
2681
+ <div class="bot-checks" id="bPerm">${permChecks}</div>
2682
+ <div class="sub" style="margin-top:4px">${_t('bot_perm_note')}</div>
2343
2683
  <label class="mem-label">${_t('bot_skills')}</label>
2344
2684
  <div class="bot-checks" id="bSkills">${skillChecks}</div>`}
2345
2685
  <label class="mem-label">${_t('bot_browser')}</label>
@@ -2387,6 +2727,18 @@ function renderBotSettings(pane, b) {
2387
2727
  if (bb) renderBotSettings(pane, bb);
2388
2728
  })
2389
2729
  );
2730
+ /* 'all' hariç diğer izinler çoklu seçilebilir; 'all' işaretlenince diğerleri kapanır */
2731
+ f.querySelectorAll('#bPerm input').forEach((c) =>
2732
+ c.addEventListener('change', () => {
2733
+ if (c.dataset.perm === 'all' && c.checked) {
2734
+ f.querySelectorAll('#bPerm input').forEach((x) => { if (x !== c) x.checked = false; });
2735
+ } else if (c.checked) {
2736
+ const all = f.querySelector('#bPerm input[data-perm="all"]');
2737
+ if (all) all.checked = false;
2738
+ }
2739
+ if (![...f.querySelectorAll('#bPerm input')].some((x) => x.checked)) c.checked = true; // en az biri seçili kalsın
2740
+ })
2741
+ );
2390
2742
  $('#bSave').addEventListener('click', async () => {
2391
2743
  const patch = {
2392
2744
  name: $('#bName').value.trim(),
@@ -2399,7 +2751,8 @@ function renderBotSettings(pane, b) {
2399
2751
  numbers: (b.numbers || []).map((n) => n.num),
2400
2752
  };
2401
2753
  if (!b.admin) {
2402
- patch.perm = $('#bPerm').value;
2754
+ const selPerms = [...f.querySelectorAll('#bPerm input:checked')].map((c) => c.dataset.perm);
2755
+ patch.perm = selPerms.includes('all') ? 'all' : selPerms;
2403
2756
  const sk = {};
2404
2757
  f.querySelectorAll('#bSkills input').forEach((c) => { sk[c.dataset.skill] = c.checked; });
2405
2758
  patch.skills = sk;
@@ -2847,6 +3200,8 @@ function onEvent(ev) {
2847
3200
  scheduleAgentsRender();
2848
3201
  return;
2849
3202
  }
3203
+ /* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
3204
+ if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
2850
3205
  /* Beast Code oturumu (IDE modu ortasındaki panel): olayları panele akıt,
2851
3206
  ana sohbeti kirletme */
2852
3207
  if (bcSessionId && ev.sessionId === bcSessionId) {
@@ -3775,6 +4130,7 @@ async function init() {
3775
4130
  }
3776
4131
 
3777
4132
  beast.onWaEvent(onWaEvent);
4133
+ beast.onTgEvent(onTgEvent);
3778
4134
 
3779
4135
  els.gearBtn.addEventListener('click', openSettings);
3780
4136
  els.setClose.addEventListener('click', closeSettings);