beast-agent 2.4.5 → 2.6.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/main.js CHANGED
@@ -229,17 +229,20 @@ startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
229
229
  try { setSearchChain(settings.searchChain); } catch {}
230
230
  try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
231
231
  let wa = null;
232
- let waChats = new Map(); // jid -> aktif session id
232
+ let waChats = new Map(); // jid -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
233
+ let waSingleSid = null; // KANAL TEK OTURUMU: WhatsApp için BİR oturum — silinmedikçe hep aynı
233
234
  let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
234
235
  let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
235
236
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
236
237
  const WA_HISTORY_CAP = 20;
237
238
  let tg = null;
238
- let tgChats = new Map(); // telegram chatId -> aktif session id
239
+ let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
240
+ let tgSingleSid = null; // KANAL TEK OTURUMU: Telegram için BİR oturum
239
241
  let tgHistory = new Map(); // chatId -> [sid,...]
240
242
  const TG_HISTORY_CAP = 20;
241
243
  let dc = null;
242
- let dcChats = new Map(); // discord channelId -> aktif session id
244
+ let dcChats = new Map(); // discord channelId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
245
+ let dcSingleSid = null; // KANAL TEK OTURUMU: Discord için BİR oturum
243
246
  let dcHistory = new Map(); // channelId -> [sid,...]
244
247
  const DC_HISTORY_CAP = 20;
245
248
  const DC_CHATS_FILE = path.join(APP_DIR, 'dc-chats.json');
@@ -278,6 +281,8 @@ try {
278
281
  if (!h.includes(s)) h.push(s);
279
282
  waHistory.set(j, h.slice(-WA_HISTORY_CAP));
280
283
  }
284
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
285
+ if (typeof raw.active === 'string' && raw.active) waSingleSid = raw.active;
281
286
  /* BeastCode modu: hangi sohbetler uzaktan kodlama yapıyor */
282
287
  if (Array.isArray(raw.bcMode)) {
283
288
  for (const j of raw.bcMode) if (typeof j === 'string') waBcMode.add(j);
@@ -293,6 +298,7 @@ function saveWaChats() {
293
298
  chats: Object.fromEntries(waChats),
294
299
  history: Object.fromEntries([...waHistory.entries()].map(([j, a]) => [j, a.slice(-WA_HISTORY_CAP)])),
295
300
  bcMode: [...waBcMode],
301
+ active: waSingleSid || '',
296
302
  })
297
303
  );
298
304
  } catch {}
@@ -1268,12 +1274,26 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1268
1274
  if (cmd === 'help') {
1269
1275
  out = waSlashHelp();
1270
1276
  } else if (cmd === 'new') {
1277
+ /* /new: kanalın TEK oturumunu kapat, yenisini aç — aynı anda iki
1278
+ WhatsApp sohbet oturumu olmaz; eski oturum silinir */
1279
+ const oldSid = waSingleSid || waChats.get(jid) || '';
1280
+ if (oldSid) {
1281
+ let olds = null;
1282
+ try { olds = engine.cache.get(oldSid) || null; } catch {}
1283
+ if (!olds || !olds.bcCode) {
1284
+ /* Beast Code panel oturumuysa dokunma — yalnız pointer taşınır */
1285
+ try { engine.deleteSession(oldSid); } catch {}
1286
+ for (const [j, h] of waHistory.entries()) waHistory.set(j, h.filter((x) => x !== oldSid));
1287
+ }
1288
+ }
1271
1289
  const v = engine.createSession();
1290
+ waSingleSid = v.id;
1291
+ for (const j of [...waChats.keys()]) waChats.set(j, v.id);
1272
1292
  waChats.set(jid, v.id);
1273
1293
  waRememberSession(jid, v.id);
1274
1294
  saveWaChats();
1275
1295
  wa.setWatchJids([...waChats.keys()]);
1276
- out = `*Yeni oturum* \`${v.code}\` açıldı — anlatabilirsin.\nDiğer oturumlara geçiş: \`/open <kod>\``;
1296
+ out = `*Yeni oturum* \`${v.code}\` açıldı — eski oturum kapatıldı, artık buradan devam.\nVar olan başka oturuma geçiş: \`/open <kod>\``;
1277
1297
  } else if (cmd === 'open') {
1278
1298
  if (!arg) {
1279
1299
  out = 'Kullanım: `/open <kod>` — kodları görmek için /sessions';
@@ -1282,7 +1302,9 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1282
1302
  if (!hit) {
1283
1303
  out = `\`${arg.toUpperCase()}\` kodlu oturum bulunamadı. Listeyi görmek için /sessions`;
1284
1304
  } else {
1285
- waChats.set(jid, hit.id);
1305
+ /* /open: kanal TEK oturumunu bu oturuma çevirir — tüm jid'ler bağlanır */
1306
+ waSingleSid = hit.id;
1307
+ for (const j of [...waChats.keys()]) waChats.set(j, hit.id);
1286
1308
  waRememberSession(jid, hit.id);
1287
1309
  saveWaChats();
1288
1310
  wa.setWatchJids([...waChats.keys()]);
@@ -1564,14 +1586,15 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1564
1586
  try { engine.interrupt(sid, 'kullanıcı /beastagent ile sohbet moduna döndü'); } catch {}
1565
1587
  }
1566
1588
  waBcMode.delete(jid);
1567
- const v = engine.createSession();
1568
- waChats.set(jid, v.id);
1569
- waRememberSession(jid, v.id);
1589
+ /* KANAL TEK OTURUMU: yeni oturum AÇILMAZ — sohbet oturumuna dönülür
1590
+ (silinmediyse hep aynı; ilk kez ise burada oluşur) */
1591
+ const chatSid = ensureWaSession(jid);
1592
+ const chatS = engine.cache.get(chatSid);
1570
1593
  saveWaChats();
1571
1594
  if (wa) wa.setWatchJids([...waChats.keys()]);
1572
1595
  emitWaEventSafe({ type: 'bc-screen', on: false });
1573
1596
  out =
1574
- `*💬 Sohbet moduna dönüldü* — masaüstünde chat ekranı açıldı, yeni oturum \`${v.code}\`.\n` +
1597
+ `*💬 Sohbet moduna dönüldü* — masaüstünde chat ekranı açıldı, sohbet oturumu \`${(chatS && chatS.code) || '?'}\`.\n` +
1575
1598
  `Kodlar duruyor: \`${waBcWorkspace()}\`\n` +
1576
1599
  `Tekrar kodlamak için: /beastcode`;
1577
1600
  }
@@ -1682,19 +1705,58 @@ function waGroupSenderInfo(payload) {
1682
1705
  return { label, name: (hitP && hitP.name) || '', isOwner: !!(hitP && hitP.owner) };
1683
1706
  }
1684
1707
 
1685
- /* Oturum yoksa oluştur (processWaMessage ile aynı kalıp) */
1708
+ /* ---------- KANAL TEK OTURUMU (WA/TG/DC) ----------
1709
+ Her kanalın BİR aktif sohbet oturumu vardır; eskisi silinmedikçe hep
1710
+ AYNI oturumdan devam edilir — Beast kendi kendine oturum kapatıp
1711
+ yenisiyle değiştirmez. Yenisi yalnızca: (1) kayıtlı oturum dosyası
1712
+ yoksa (kullanıcı UI'dan sildi), (2) kullanıcı WA'da /new yazarsa.
1713
+ Masaüstü chat UI'daki "+ Yeni Sohbet" bu kuralın dışındadır. */
1714
+ function sessionFileAlive(sid) {
1715
+ try {
1716
+ return !!sid && fs.existsSync(path.join(engine.sessionsDir, String(sid) + '.jsonl'));
1717
+ } catch {
1718
+ return false;
1719
+ }
1720
+ }
1721
+
1722
+ /* kanalın yaşayan tek oturumunu döndürür; geçersizse eski bağlamdan devralır,
1723
+ o da yoksa (ilk kullanım) yeni açar ve tüm jid'leri ona bağlar */
1686
1724
  function ensureWaSession(jid) {
1687
- let sid = waChats.get(jid);
1688
- if (!sid) {
1689
- const v = engine.createSession();
1690
- sid = v.id;
1691
- waChats.set(jid, sid);
1725
+ if (!waSingleSid || !sessionFileAlive(waSingleSid)) {
1726
+ let best = '';
1727
+ let bestTs = '';
1728
+ for (const c of new Set([...waChats.values(), waSingleSid])) {
1729
+ if (!c || !sessionFileAlive(c)) continue;
1730
+ let ts = '';
1731
+ try {
1732
+ const s = engine.cache.get(c) || engine._load(c);
1733
+ ts = String((s && s.updatedAt) || '');
1734
+ } catch {}
1735
+ if (!best || ts > bestTs) {
1736
+ best = c;
1737
+ bestTs = ts;
1738
+ }
1739
+ }
1740
+ waSingleSid = best || engine.createSession().id;
1741
+ saveWaChats();
1742
+ }
1743
+ let dirty = false;
1744
+ for (const [j, s] of waChats.entries()) {
1745
+ if (s !== waSingleSid) {
1746
+ waChats.set(j, waSingleSid);
1747
+ dirty = true;
1748
+ }
1749
+ }
1750
+ if (jid != null && waChats.get(String(jid)) !== waSingleSid) {
1751
+ waChats.set(String(jid), waSingleSid);
1752
+ dirty = true;
1753
+ }
1754
+ waRememberSession(String(jid), waSingleSid);
1755
+ if (dirty) {
1692
1756
  saveWaChats();
1693
1757
  if (wa) wa.setWatchJids([...waChats.keys()]);
1694
- } else {
1695
- waRememberSession(jid, sid);
1696
1758
  }
1697
- return sid;
1759
+ return waSingleSid;
1698
1760
  }
1699
1761
 
1700
1762
  /* ---------- GRUP BAĞLAM AKIŞI (mentionOnly + seeAll) ----------
@@ -2232,7 +2294,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2232
2294
  waLog(`skip flush: izinli eşleşme yok (sender=+${senderNum || '?'})`);
2233
2295
  return;
2234
2296
  }
2235
- let sid = waChats.get(jid);
2297
+ let sid = waChats.get(jid) || waSingleSid;
2236
2298
  if (sid && engine.isBusy(sid)) {
2237
2299
  /* oturum hâlâ önceki işle uğraşıyor — KAYBETME: sınırı kaldırdık,
2238
2300
  iş bitene dek her debounce turunda yeniden deneriz; kullanıcıya
@@ -2246,16 +2308,8 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2246
2308
  if (!isGroup && requeues > 0) {
2247
2309
  // retry sonrası izin yeniden kontrol edilir (yukarıda hit zaten alınıyor)
2248
2310
  }
2249
- if (!sid) {
2250
- const v = engine.createSession();
2251
- sid = v.id;
2252
- waChats.set(jid, sid);
2253
- waRememberSession(jid, sid);
2254
- saveWaChats();
2255
- if (wa) wa.setWatchJids([...waChats.keys()]);
2256
- } else {
2257
- waRememberSession(jid, sid);
2258
- }
2311
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2312
+ sid = ensureWaSession(jid);
2259
2313
  // Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
2260
2314
  wa.setComposing(jid, true);
2261
2315
  // Kişi bazlı granül izin: all/web/read/chat
@@ -2462,6 +2516,8 @@ function ensureWa() {
2462
2516
  if (Array.isArray(arr)) tgHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-TG_HISTORY_CAP));
2463
2517
  }
2464
2518
  }
2519
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
2520
+ if (raw && typeof raw.active === 'string' && raw.active) tgSingleSid = raw.active;
2465
2521
  for (const [c, s] of tgChats.entries()) {
2466
2522
  const h = tgHistory.get(c) || [];
2467
2523
  if (!h.includes(s)) h.push(s);
@@ -2477,6 +2533,7 @@ function saveTgChats() {
2477
2533
  JSON.stringify({
2478
2534
  chats: Object.fromEntries(tgChats),
2479
2535
  history: Object.fromEntries([...tgHistory.entries()].map(([c, a]) => [c, a.slice(-TG_HISTORY_CAP)])),
2536
+ active: tgSingleSid || '',
2480
2537
  })
2481
2538
  );
2482
2539
  } catch {}
@@ -2549,21 +2606,14 @@ async function processTgMessage(chatId, payload, requeues = 0) {
2549
2606
  tgLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2550
2607
  return;
2551
2608
  }
2552
- let sid = tgChats.get(chatId);
2609
+ let sid = tgChats.get(chatId) || tgSingleSid;
2553
2610
  if (sid && engine.isBusy(sid)) {
2554
2611
  /* oturum meşgul — WA ile aynı: kaybetme, iş bitene dek yeniden dene */
2555
2612
  await new Promise((r) => setTimeout(r, TG_DEBOUNCE_MS));
2556
2613
  return processTgMessage(chatId, payload, requeues + 1);
2557
2614
  }
2558
- if (!sid) {
2559
- const v = engine.createSession();
2560
- sid = v.id;
2561
- tgChats.set(chatId, sid);
2562
- tgRememberSession(chatId, sid);
2563
- saveTgChats();
2564
- } else {
2565
- tgRememberSession(chatId, sid);
2566
- }
2615
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2616
+ sid = ensureTgSession(chatId);
2567
2617
  /* Kişi bazlı granül izin: all/web/read/chat */
2568
2618
  let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
2569
2619
  engine.setSessionPerm(sid, perm);
@@ -2686,6 +2736,8 @@ function dcFind(senderId, username) {
2686
2736
  if (Array.isArray(arr)) dcHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-DC_HISTORY_CAP));
2687
2737
  }
2688
2738
  }
2739
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
2740
+ if (raw && typeof raw.active === 'string' && raw.active) dcSingleSid = raw.active;
2689
2741
  for (const [c, s] of dcChats.entries()) {
2690
2742
  const h = dcHistory.get(c) || [];
2691
2743
  if (!h.includes(s)) h.push(s);
@@ -2701,6 +2753,7 @@ function saveDcChats() {
2701
2753
  JSON.stringify({
2702
2754
  chats: Object.fromEntries(dcChats),
2703
2755
  history: Object.fromEntries([...dcHistory.entries()].map(([c, a]) => [c, a.slice(-DC_HISTORY_CAP)])),
2756
+ active: dcSingleSid || '',
2704
2757
  })
2705
2758
  );
2706
2759
  } catch {}
@@ -2767,21 +2820,14 @@ async function processDcMessage(channelId, payload) {
2767
2820
  dcLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2768
2821
  return;
2769
2822
  }
2770
- let sid = dcChats.get(channelId);
2823
+ let sid = dcChats.get(channelId) || dcSingleSid;
2771
2824
  if (sid && engine.isBusy(sid)) {
2772
2825
  /* oturum meşgul — WA/TG ile aynı: kaybetme, iş bitene dek yeniden dene */
2773
2826
  await new Promise((r) => setTimeout(r, DC_DEBOUNCE_MS));
2774
2827
  return processDcMessage(channelId, payload, 1);
2775
2828
  }
2776
- if (!sid) {
2777
- const v = engine.createSession();
2778
- sid = v.id;
2779
- dcChats.set(channelId, sid);
2780
- dcRememberSession(channelId, sid);
2781
- saveDcChats();
2782
- } else {
2783
- dcRememberSession(channelId, sid);
2784
- }
2829
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2830
+ sid = ensureDcSession(channelId);
2785
2831
  /* Kişi bazlı granül izin: all/web/read/chat */
2786
2832
  let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
2787
2833
  engine.setSessionPerm(sid, perm);
@@ -3404,7 +3450,7 @@ function isDevServerUrl(url) {
3404
3450
  /* Tarayıcı state: visible=false → ajanlar GİZLİ kullanır (headless);
3405
3451
  göz ikonuyla görünür mod açılır. open=view aktif, visible=panelde görünürlük */
3406
3452
  const browser = { view: null, open: false, visible: false, width: 0, attached: false, started: false, phone: false, mobile: false, mobileRect: null, deviceKey: 'iphone14', bottomInset: 0, desktopUA: '' };
3407
- browser.mobile = settings.browserMobile === true; // mobil önizleme tercihi kalıcı
3453
+ browser.mobile = false; // mobil önizleme (telefon silueti) şimdilik devre dışı
3408
3454
  if (PHONE_DEVICES[settings.browserDevice]) browser.deviceKey = settings.browserDevice;
3409
3455
 
3410
3456
  function browserEmit(payload) {
@@ -4584,19 +4630,37 @@ function resyncBrowserUi() {
4584
4630
  }
4585
4631
  }
4586
4632
 
4587
- win.on('resize', layoutBrowser);
4633
+ win.on('resize', () => {
4634
+ layoutBrowser();
4635
+ /* resize sürerken son boyutu yakala: maximize/restore bitiminde dock KESİN
4636
+ yeni pencere boyutuna otursun (BrowserView bayat bounds bırakmasın) */
4637
+ clearTimeout(win._bzRzT);
4638
+ win._bzRzT = setTimeout(() => { try { layoutBrowser(); } catch {} }, 120);
4639
+ });
4588
4640
  win.on('maximize', () => {
4589
4641
  layoutBrowser();
4590
4642
  resyncBrowserUi();
4643
+ setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 80);
4644
+ setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 300);
4591
4645
  try { if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'win-max', maximized: true }); } catch {}
4592
4646
  });
4593
4647
  win.on('unmaximize', () => {
4594
4648
  layoutBrowser();
4595
4649
  resyncBrowserUi();
4650
+ setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 80);
4651
+ setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 300);
4596
4652
  try { if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'win-max', maximized: false }); } catch {}
4597
4653
  });
4598
- win.on('enter-full-screen', () => { layoutBrowser(); resyncBrowserUi(); });
4599
- win.on('leave-full-screen', () => { layoutBrowser(); resyncBrowserUi(); });
4654
+ win.on('enter-full-screen', () => {
4655
+ layoutBrowser(); resyncBrowserUi();
4656
+ setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 80);
4657
+ setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 300);
4658
+ });
4659
+ win.on('leave-full-screen', () => {
4660
+ layoutBrowser(); resyncBrowserUi();
4661
+ setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 80);
4662
+ setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 300);
4663
+ });
4600
4664
  win.on('show', layoutBrowser);
4601
4665
  // X'e basınca gizle — tepside yaşamaya devam, WhatsApp bağlantısı sürer
4602
4666
  win.on('close', (e) => {
@@ -6201,9 +6265,10 @@ function empatiCfg() {
6201
6265
  return empati.mergeCfg(settings.empati || {});
6202
6266
  }
6203
6267
 
6204
- /* EMPATİ HAFIZASI: sohbet akışını kaydet + kullanıcı ilgi alanlarını öğren.
6205
- - user mesajı → hafızaya yaz + sık kelimelerden ilgi etiketi öğren
6206
- - assistant cevabı hafızaya yaz (konuşmanın iki yanı da kalsın)
6268
+ /* EMPATİ HAFIZASI: sohbet akışını kaydet (ilgi çıkarımının ham verisi).
6269
+ - user/assistant mesajı → hafızaya yaz (konuşmanın iki yanı da kalsın)
6270
+ İlgi alanları burada MESAJ BAŞINA öğrenilmez gece yansımasında
6271
+ (günde bir) LLM bu birikimden konu etiketleri çıkarır (nightref adım 3b).
6207
6272
  Beast Code/Studio iş mesajları, /komutlar ve bağlam enjeksiyonları kayda girmez. */
6208
6273
  function empatiRememberFromEvent(ev) {
6209
6274
  if (!ev || ev.type !== 'message' || !ev.message || !ev.message.role) return;
@@ -6222,7 +6287,6 @@ function empatiRememberFromEvent(ev) {
6222
6287
  if (bc) return; /* kod işi — ilgi alanını bulanıklaştırır */
6223
6288
  if (m.role === 'user') {
6224
6289
  empati.rememberConversation(txt, 'sohbet');
6225
- empati.learnInterests(txt);
6226
6290
  } else {
6227
6291
  empati.rememberConversation(txt, 'beast');
6228
6292
  }
@@ -6261,7 +6325,9 @@ async function empatiSignalSelf() {
6261
6325
  }
6262
6326
 
6263
6327
  async function empatiSignalNews() {
6264
- const topics = String(empatiCfg().newsTopics || '').split(',').map((s) => s.trim()).filter(Boolean);
6328
+ /* haber konuları İlgi Alanları'ndan OTOMATİK türetilir — seçime gerek yok */
6329
+ const topics = String(empati.combinedInterests(empatiCfg()) || '')
6330
+ .split(',').map((s) => s.trim()).filter((s) => s.length > 2);
6265
6331
  if (!topics.length) return [];
6266
6332
  return empati.fetchNews(topics);
6267
6333
  }
@@ -6309,6 +6375,22 @@ function empatiLlmCompose(prompt) {
6309
6375
  KENDİSİNİN attığını bilir — aynı oturum, kesintisiz bağlam. */
6310
6376
  const PROACTIVE_MARK = '🫡 *Beast proaktif:*';
6311
6377
 
6378
+ /* bildirimin ALTINA YAZILAN KAYNAK: haberin gerçek linki varsa o, yoksa kaynak adı.
6379
+ Kullanıcı "bu proje neymiş?" diye sorduğunda linkten bulabilsin.
6380
+ TEK SATIR kuralı: satır sonu/boşluklar temizlenir, uzunsa sona "..." gelir. */
6381
+ function empatiSourceLine(ev) {
6382
+ if (!ev) return '';
6383
+ const oneLine = (s) =>
6384
+ String(s || '').replace(/\s*\n+\s*/g, ' ').replace(/\s{2,}/g, ' ').trim();
6385
+ const ELLIPSIS = '...';
6386
+ const cap = (s, max) =>
6387
+ oneLine(s).length > max ? oneLine(s).slice(0, max - ELLIPSIS.length) + ELLIPSIS : oneLine(s);
6388
+ const url = oneLine(ev.url);
6389
+ if (/^https?:\/\//i.test(url)) return '🔗 ' + cap(url, 100);
6390
+ const src = oneLine(ev.source);
6391
+ return src ? '🔗 kaynak: ' + cap(src, 80) : '';
6392
+ }
6393
+
6312
6394
  function empatiInjectToSession(sid, out) {
6313
6395
  try {
6314
6396
  if (!sid || engine.isBusy(sid)) return; /* tur ortasında geçmişi karıştırma — bildirim kanala zaten gitti */
@@ -6316,28 +6398,88 @@ function empatiInjectToSession(sid, out) {
6316
6398
  } catch {}
6317
6399
  }
6318
6400
 
6319
- /* Oturum yoksa oluştur (processTgMessage/processDcMessage ile aynı kalıp) */
6401
+ /* kanala giden metin + Beast'in İÇ bağlamı ayrıdır: kullanıcı linki silse bile
6402
+ model olayın başlığını, detayını ve linkini KENDİSİ görür — "bu neymiş?"
6403
+ sorusuna insansı cevap verebilir */
6404
+ function empatiInjectText(ev, out) {
6405
+ if (!ev) return out;
6406
+ return (
6407
+ out + '\n\n[BAĞLAM — kullanıcı bu bildirimi sorarsa bununla cevapla]\n' +
6408
+ 'Olay: ' + String(ev.title || '').slice(0, 200) + '\n' +
6409
+ (ev.detail ? 'Detay: ' + String(ev.detail).slice(0, 200) + '\n' : '') +
6410
+ (ev.url ? 'Link: ' + String(ev.url) + '\n' : '') +
6411
+ 'Kaynak: ' + String(ev.source || '').slice(0, 80)
6412
+ );
6413
+ }
6414
+
6415
+ /* kanalın yaşayan tek oturumu (WA kuralının aynısı — Telegram/Discord) */
6320
6416
  function ensureTgSession(chatId) {
6321
- let sid = tgChats.get(chatId);
6322
- if (!sid) {
6323
- const v = engine.createSession();
6324
- sid = v.id;
6325
- tgChats.set(chatId, sid);
6326
- tgRememberSession(chatId, sid);
6417
+ if (!tgSingleSid || !sessionFileAlive(tgSingleSid)) {
6418
+ let best = '';
6419
+ let bestTs = '';
6420
+ for (const c of new Set([...tgChats.values(), tgSingleSid])) {
6421
+ if (!c || !sessionFileAlive(c)) continue;
6422
+ let ts = '';
6423
+ try {
6424
+ const s = engine.cache.get(c) || engine._load(c);
6425
+ ts = String((s && s.updatedAt) || '');
6426
+ } catch {}
6427
+ if (!best || ts > bestTs) {
6428
+ best = c;
6429
+ bestTs = ts;
6430
+ }
6431
+ }
6432
+ tgSingleSid = best || engine.createSession().id;
6327
6433
  saveTgChats();
6328
6434
  }
6329
- return sid;
6435
+ let dirty = false;
6436
+ for (const [c, s] of tgChats.entries()) {
6437
+ if (s !== tgSingleSid) {
6438
+ tgChats.set(c, tgSingleSid);
6439
+ dirty = true;
6440
+ }
6441
+ }
6442
+ if (chatId != null && tgChats.get(String(chatId)) !== tgSingleSid) {
6443
+ tgChats.set(String(chatId), tgSingleSid);
6444
+ dirty = true;
6445
+ }
6446
+ tgRememberSession(String(chatId), tgSingleSid);
6447
+ if (dirty) saveTgChats();
6448
+ return tgSingleSid;
6330
6449
  }
6331
6450
  function ensureDcSession(channelId) {
6332
- let sid = dcChats.get(channelId);
6333
- if (!sid) {
6334
- const v = engine.createSession();
6335
- sid = v.id;
6336
- dcChats.set(channelId, sid);
6337
- dcRememberSession(channelId, sid);
6451
+ if (!dcSingleSid || !sessionFileAlive(dcSingleSid)) {
6452
+ let best = '';
6453
+ let bestTs = '';
6454
+ for (const c of new Set([...dcChats.values(), dcSingleSid])) {
6455
+ if (!c || !sessionFileAlive(c)) continue;
6456
+ let ts = '';
6457
+ try {
6458
+ const s = engine.cache.get(c) || engine._load(c);
6459
+ ts = String((s && s.updatedAt) || '');
6460
+ } catch {}
6461
+ if (!best || ts > bestTs) {
6462
+ best = c;
6463
+ bestTs = ts;
6464
+ }
6465
+ }
6466
+ dcSingleSid = best || engine.createSession().id;
6338
6467
  saveDcChats();
6339
6468
  }
6340
- return sid;
6469
+ let dirty = false;
6470
+ for (const [c, s] of dcChats.entries()) {
6471
+ if (s !== dcSingleSid) {
6472
+ dcChats.set(c, dcSingleSid);
6473
+ dirty = true;
6474
+ }
6475
+ }
6476
+ if (channelId != null && dcChats.get(String(channelId)) !== dcSingleSid) {
6477
+ dcChats.set(String(channelId), dcSingleSid);
6478
+ dirty = true;
6479
+ }
6480
+ dcRememberSession(String(channelId), dcSingleSid);
6481
+ if (dirty) saveDcChats();
6482
+ return dcSingleSid;
6341
6483
  }
6342
6484
 
6343
6485
  /* Masaüstü yedeği: en güncel (bg/bot-DM'siz, meşgul olmayan) sohbet — yoksa yeni */
@@ -6354,16 +6496,18 @@ function empatiDesktopSid() {
6354
6496
  Hiçbir entegrasyon yazılamazsa masaüstü chat UI (toast) kalır. */
6355
6497
  function empatiNotify(text, ev) {
6356
6498
  const cfg = empatiCfg();
6499
+ const src = empatiSourceLine(ev);
6500
+ const out = PROACTIVE_MARK + '\n' + text + (src ? '\n' + src : '');
6501
+ const inject = empatiInjectText(ev, out);
6357
6502
  const senders = [];
6358
6503
  const tryWa = () => {
6359
6504
  try {
6360
6505
  const own = waOwnerNum();
6361
6506
  if (own && wa && wa.connected) {
6362
6507
  const jid = own + '@s.whatsapp.net';
6363
- const out = PROACTIVE_MARK + '\n' + text;
6364
6508
  senders.push(() =>
6365
6509
  Promise.resolve(sendWaSafe(jid, out))
6366
- .then(() => empatiInjectToSession(ensureWaSession(jid), out))
6510
+ .then(() => empatiInjectToSession(ensureWaSession(jid), inject))
6367
6511
  .catch(() => {})
6368
6512
  );
6369
6513
  }
@@ -6372,11 +6516,10 @@ function empatiNotify(text, ev) {
6372
6516
  const tryTg = () => {
6373
6517
  try {
6374
6518
  if (tg && tg.connected) {
6375
- const out = PROACTIVE_MARK + '\n' + text;
6376
6519
  for (const id of tgOwnerIds()) {
6377
6520
  senders.push(() =>
6378
6521
  Promise.resolve(sendTgSafe(id, out))
6379
- .then(() => empatiInjectToSession(ensureTgSession(String(id)), out))
6522
+ .then(() => empatiInjectToSession(ensureTgSession(String(id)), inject))
6380
6523
  .catch(() => {})
6381
6524
  );
6382
6525
  }
@@ -6386,11 +6529,11 @@ function empatiNotify(text, ev) {
6386
6529
  const tryDc = () => {
6387
6530
  try {
6388
6531
  if (dc && dc.connected) {
6389
- const out = '🫡 **Beast proaktif:**\n' + text;
6532
+ const outDc = out.replace('🫡 *Beast proaktif:*', '🫡 **Beast proaktif:**');
6390
6533
  for (const id of dcOwnerIds()) {
6391
6534
  senders.push(() =>
6392
- Promise.resolve(sendDcSafe(id, out))
6393
- .then(() => empatiInjectToSession(ensureDcSession(String(id)), out))
6535
+ Promise.resolve(sendDcSafe(id, outDc))
6536
+ .then(() => empatiInjectToSession(ensureDcSession(String(id)), inject))
6394
6537
  .catch(() => {})
6395
6538
  );
6396
6539
  }
@@ -6409,8 +6552,8 @@ function empatiNotify(text, ev) {
6409
6552
  metin güncel sohbete de asistan mesajı olarak işlenir */
6410
6553
  try {
6411
6554
  if (!sent && win && !win.isDestroyed()) {
6412
- empatiInjectToSession(empatiDesktopSid(), PROACTIVE_MARK + '\n' + text);
6413
- win.webContents.send('agent:event', { type: 'proactive', id: ev.id, level: ev.level, title: ev.title, text });
6555
+ empatiInjectToSession(empatiDesktopSid(), inject);
6556
+ win.webContents.send('agent:event', { type: 'proactive', id: ev.id, level: ev.level, title: ev.title, text: out });
6414
6557
  }
6415
6558
  } catch {}
6416
6559
  }
@@ -6798,7 +6941,8 @@ function termShellSpawn() {
6798
6941
  }
6799
6942
 
6800
6943
  ipcMain.handle('terminal:toggle', () => {
6801
- if (browser.open) setBrowserOpen(false);
6944
+ /* terminal artık tarayıcıyla AYNI alanı paylaşmıyor (Beast Code'da kod
6945
+ bölmesinde) — açılışı/kapanışı tarayıcıya DOKUNMAZ */
6802
6946
  return { ok: true, cwd: termShellCwd || (engine && engine.workspace) || settings.workspace || app.getPath('home') };
6803
6947
  });
6804
6948
 
@@ -6895,11 +7039,23 @@ function bcGetSession(folder) {
6895
7039
  JSON.stringify({ t: 'meta2', bgOf: '', title: 'Beast Code', at: new Date().toISOString() }) + '\n'
6896
7040
  );
6897
7041
  } catch {}
7042
+ bcMarkWs(s, folder);
6898
7043
  engine.cache.set(s.id, s);
6899
7044
  bcSessions.set(folder, s.id);
6900
7045
  return s;
6901
7046
  }
6902
7047
 
7048
+ /* Beast Code oturumuna çalışma klasörünü işle: sohbet geçmişi listesinde
7049
+ hangi klasörde çalışıldığı görünür; oturum tekrar açılınca aynı klasöre
7050
+ bağlanır. Kayıt oturum dosyasına bcws satırı olarak YAZILIR. */
7051
+ function bcMarkWs(s, folder) {
7052
+ try {
7053
+ if (s.bcWs === folder) return;
7054
+ s.bcWs = folder;
7055
+ fs.appendFileSync(engine._file(s.id), JSON.stringify({ t: 'bcws', ws: folder, at: new Date().toISOString() }) + '\n');
7056
+ } catch {}
7057
+ }
7058
+
6903
7059
  /* Beast Code motoru: TAMAMEN BEAST MOTORU — opencode'in döngü mantığı
6904
7060
  (compaction, prune, prompt-cache, doom-loop, yetim onarım, proje
6905
7061
  talimatları) engine.js'e native port edildi; köprü/alt süreç yok. */
@@ -7071,6 +7227,85 @@ ipcMain.handle('beastcode:new', async () => {
7071
7227
  return { ok: true };
7072
7228
  });
7073
7229
 
7230
+ /* ---------------- Beast Code SOHBET GEÇMİŞİ ----------------
7231
+ Soldaki dosya panelinin alt yarısında eski Beast Code oturumları listelenir.
7232
+ Ana sohbet geçmişinden (bgTitle gizli oturumlar) ve Studio'dan TAMAMEN AYRIDIR.
7233
+ Oturuma tıklayınca panele açılır, kaldığı yerden devam edilir. */
7234
+ ipcMain.handle('bc:history', async () => {
7235
+ try {
7236
+ return { ok: true, items: engine.listBcSessions(100) };
7237
+ } catch (e) {
7238
+ return { ok: false, error: String((e && e.message) || e), items: [] };
7239
+ }
7240
+ });
7241
+
7242
+ function bcMsgText(m) {
7243
+ const t = Array.isArray(m && m.content)
7244
+ ? m.content.filter((p) => p && p.type === 'text').map((p) => String(p.text || '')).join('\n')
7245
+ : String((m && m.content) || '');
7246
+ return t.replace(/\s+/g, ' ').trim();
7247
+ }
7248
+
7249
+ ipcMain.handle('bc:open', async (_e, payload) => {
7250
+ const id = String((payload && payload.id) || '');
7251
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
7252
+ if (!id) return { ok: false, error: 'oturum belirtilmedi' };
7253
+ let s;
7254
+ try { s = engine._load(id); } catch { return { ok: false, error: 'oturum açılamadı' }; }
7255
+ if (s.bgTitle && s.bgTitle !== 'Beast Code') return { ok: false, error: 'bu oturum Beast Code oturumu değil' };
7256
+ /* mevcut klasöre bağlan: sonraki mesajlar BU oturumda sürer */
7257
+ const ws = ideRoot();
7258
+ s.workspace = ws;
7259
+ s.bcCode = true;
7260
+ bcMarkWs(s, ws);
7261
+ engine.cache.set(s.id, s);
7262
+ bcSessions.set(ws, s.id);
7263
+ const msgs = [];
7264
+ for (const m of s.messages || []) {
7265
+ if (m.tool_calls) continue;
7266
+ const txt = bcMsgText(m);
7267
+ if (!txt) continue;
7268
+ msgs.push({ role: m.role === 'assistant' ? 'assistant' : 'user', text: txt.slice(0, 4000) });
7269
+ }
7270
+ return {
7271
+ ok: true,
7272
+ sessionId: s.id,
7273
+ busy: !!engine.isBusy(s.id),
7274
+ mode: s.bcMode || '',
7275
+ messages: msgs.slice(-200),
7276
+ };
7277
+ });
7278
+
7279
+ /* TEK oturum silme (soldaki geçmiş listesindeki × butonu) */
7280
+ ipcMain.handle('bc:delete', async (_e, payload) => {
7281
+ const id = String((payload && payload.id) || '');
7282
+ if (!engine || !id) return { ok: false, error: 'oturum belirtilmedi' };
7283
+ let s;
7284
+ try { s = engine._load(id); } catch { return { ok: false, error: 'oturum açılamadı' }; }
7285
+ if (s.bgTitle && s.bgTitle !== 'Beast Code') return { ok: false, error: 'yalnız Beast Code oturumu silinir' };
7286
+ if (engine.isBusy(id)) return { ok: false, error: 'oturum çalışıyor — önce ■ ile durdur' };
7287
+ for (const [folder, sid] of [...bcSessions.entries()]) {
7288
+ if (String(sid) === id) bcSessions.delete(folder);
7289
+ }
7290
+ try { engine.deleteSession(id); } catch {}
7291
+ return { ok: true };
7292
+ });
7293
+
7294
+ /* TÜM Beast Code oturumlarını sil — çalışanlar atlanır */
7295
+ ipcMain.handle('bc:deleteAll', async () => {
7296
+ if (!engine) return { ok: false, error: 'ajan hazır değil' };
7297
+ let deleted = 0;
7298
+ let skipped = 0;
7299
+ for (const it of engine.listBcSessions(200)) {
7300
+ if (engine.isBusy(it.id)) { skipped++; continue; }
7301
+ for (const [folder, sid] of [...bcSessions.entries()]) {
7302
+ if (String(sid) === it.id) bcSessions.delete(folder);
7303
+ }
7304
+ try { engine.deleteSession(it.id); deleted++; } catch {}
7305
+ }
7306
+ return { ok: true, deleted, skipped };
7307
+ });
7308
+
7074
7309
  /* ---------------- Beast Studio paneli (video yapma/düzenleme modu) ----------------
7075
7310
  Beast Code ile AYRI ve ÖZEL dünya: kendi çalışma klasörü (studioRoot), kendi
7076
7311
  gizli engine oturumları (klasör bazlı), kendi sohbet kuyruğu. Studio
@@ -7901,27 +8136,8 @@ ipcMain.handle('ide:previewFile', async (_e, rel) => {
7901
8136
  ipcMain.handle('ide:preview', async () => {
7902
8137
  try {
7903
8138
  const root = ideRoot();
7904
- /* MOBİL UYGULAMA PROJESİ (expo/react-native): statik sunucu değil,
7905
- `npm run web` başlat telefon silueti içinde canlı önizleme */
7906
- const mob = ideMobileProject(root);
7907
- if (mob.mobile) {
7908
- let url = '';
7909
- /* ajanın kendi dev sunucusu canlıysa öncelikli */
7910
- if (bcLastServerUrl && isDevServerUrl(bcLastServerUrl)) {
7911
- const alive = await probeDevUrl(bcLastServerUrl, 800).catch(() => false);
7912
- if (alive) url = bcLastServerUrl;
7913
- }
7914
- if (!url) {
7915
- const r = await mobilePreviewStart(root);
7916
- if (!r.ok) return { ok: false, error: r.error };
7917
- url = r.url;
7918
- }
7919
- setBrowserMobile(true); /* siluet + görünür tarayıcı */
7920
- try { browser.view.webContents.loadURL(url).catch(() => {}); } catch {}
7921
- browserEmit({ open: true, width: browserShownWidth(browserW()), url, mobile: true, phoneRect: browser.mobileRect });
7922
- bcTellMobileServe(url);
7923
- return { ok: true, url, mobile: true };
7924
- }
8139
+ /* MOBİL ÖNİZLEME kaldırıldı: mobil projeler de normal tarayıcı dock'unda
8140
+ açılır ajanın dev sunucusu varsa o adres, yoksa dahili statik sunucu. */
7925
8141
  const pick = (name) => {
7926
8142
  const p = path.join(root, name);
7927
8143
  try { return fs.existsSync(p) ? p : null; } catch { return null; }
@@ -7982,10 +8198,7 @@ ipcMain.handle('ide:previewUrl', async (_e, url) => {
7982
8198
  } else {
7983
8199
  return { ok: false, error: 'yalnız localhost adresleri önizlenebilir' };
7984
8200
  }
7985
- /* Expo/Metro dev sunucusu telefon modu OTOMATİK (mobil uygulama canlı önizleme) */
7986
- if (/^https?:\/\/(?:localhost|127\.0\.0\.1):(8081|19000|19001|19002|3000|5173)\//i.test(target)) {
7987
- if (!browser.phone) setBrowserPhone(true);
7988
- }
8201
+ /* mobil önizleme otomatik telefon modu kaldırıldı önizleme normal dockta açılır */
7989
8202
  /* forceVisible: otomatik ve GÖRÜNÜR açılır */
7990
8203
  setBrowserOpen(true, true);
7991
8204
  browser.view.webContents.loadURL(target).catch(() => {});