beast-agent 0.28.0 → 0.30.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.
@@ -40,6 +40,7 @@ const els = {
40
40
  todoPanel: $('#todoPanel'),
41
41
  browserBtn: $('#browserBtn'),
42
42
  eyeBtn: $('#eyeBtn'),
43
+ netDot: $('#netDot'),
43
44
  railBtn: $('#railBtn'),
44
45
  watchBtn: $('#watchBtn'),
45
46
  cronBtn: $('#cronBtn'),
@@ -199,6 +200,14 @@ let netOnline = true;
199
200
  let netQueueCount = 0;
200
201
  const netPending = []; // { key, el } — bekleyen mesaj balonları
201
202
 
203
+ /* wifi göstergesi: bağlı = yeşil, kopuk = kırmızı */
204
+ function paintNetDot() {
205
+ if (!els.netDot) return;
206
+ els.netDot.classList.toggle('net-on', netOnline);
207
+ els.netDot.classList.toggle('net-off', !netOnline);
208
+ els.netDot.title = _t(netOnline ? 'tip_net_online' : 'tip_net_offline');
209
+ }
210
+
202
211
  function setNetBadge(el, text) {
203
212
  if (!el || !el.isConnected) return;
204
213
  let badge = el.querySelector('.q-badge');
@@ -223,6 +232,7 @@ function onNetEvent(ev) {
223
232
  if (ev.type === 'net') {
224
233
  const was = netOnline;
225
234
  netOnline = ev.online !== false;
235
+ paintNetDot();
226
236
  if (was !== netOnline) {
227
237
  if (netOnline) {
228
238
  toast(_t('net_online'));
@@ -270,8 +280,14 @@ function renderTodos(items) {
270
280
  })
271
281
  .join('');
272
282
  els.todoPanel.innerHTML =
273
- `<div class="td-head"><span>GÖREVLER</span><span class="td-count">${done}/${list.length}</span></div>` + rows;
283
+ `<div class="td-head"><span>GÖREVLER</span>` +
284
+ `<span class="td-right"><span class="td-count">${done}/${list.length}</span>` +
285
+ `<button class="td-x" title="${_t('td_hide')}">×</button></span></div>` + rows;
274
286
  els.todoPanel.hidden = false;
287
+ /* X yalnız BU listeyi gizler — kalıcı kapanma yok: bir sonraki görev
288
+ güncellemesi (yeni görev eklendi / görev yapıldı) paneli yeniden gösterir */
289
+ const x = els.todoPanel.querySelector('.td-x');
290
+ if (x) x.addEventListener('click', () => { els.todoPanel.hidden = true; });
275
291
  }
276
292
  function setBusy(b) {
277
293
  busy = b;
@@ -472,7 +488,41 @@ function finishToolCard(callId, ok, result) {
472
488
 
473
489
  /* ---------------- sessions sidebar ---------------- */
474
490
 
491
+ /* sol panel sohbet listesi: kullanıcı ELLE sıralayabilir (sürükle-bırak).
492
+ sessionOrder: session id'lerinin tercih sırası — settings.json'da kalıcı.
493
+ Listede olmayan oturumlar engine sırasıyla (son mesaja göre) arkada gelir. */
494
+ let sessionOrder = [];
495
+ let sessionOrderLoaded = null;
496
+ function loadSessionOrder() {
497
+ if (!sessionOrderLoaded) {
498
+ sessionOrderLoaded = beast
499
+ .sessionsOrderGet()
500
+ .then((r) => { sessionOrder = (r && Array.isArray(r.order) ? r.order : []).map(String); })
501
+ .catch(() => {});
502
+ }
503
+ return sessionOrderLoaded;
504
+ }
505
+
506
+ function reorderSessions(dragId, targetId) {
507
+ const idx = (id) => sessionOrder.indexOf(id);
508
+ const dp = idx(dragId);
509
+ const tp = idx(targetId);
510
+ sessionOrder = sessionOrder.filter((x) => x !== dragId);
511
+ const ti = sessionOrder.indexOf(targetId);
512
+ if (ti === -1) {
513
+ /* hedef hiç elle sıralanmamış: listeden görünür konumunu korumak mümkün değil — sona ekle */
514
+ sessionOrder.push(targetId, dragId);
515
+ } else if (dp !== -1 && dp < tp) {
516
+ sessionOrder.splice(ti + 1, 0, dragId); /* aşağı sürüklendi → hedefin ALTINA */
517
+ } else {
518
+ sessionOrder.splice(ti, 0, dragId); /* yukarı sürüklendi → hedefin ÜSTÜNE */
519
+ }
520
+ beast.sessionsOrderSet(sessionOrder).catch(() => {});
521
+ refreshSessions();
522
+ }
523
+
475
524
  async function renderSessions(list) {
525
+ await loadSessionOrder();
476
526
  let waSet = new Set();
477
527
  let tgSet = new Set();
478
528
  try {
@@ -482,13 +532,19 @@ async function renderSessions(list) {
482
532
  tgSet = new Set(await beast.tgListSessions());
483
533
  } catch {}
484
534
  els.sessList.innerHTML = '';
535
+ /* elle sıra: sessionOrder'daki id'ler önde (o sırayla), diğerleri engine sırasıyla arkada */
536
+ const rank = new Map(sessionOrder.map((id, i) => [id, i]));
537
+ const withRank = list.map((s, i) => ({ s, r: rank.has(s.id) ? rank.get(s.id) : 1e6 + i }));
538
+ withRank.sort((a, b) => a.r - b.r);
485
539
  /* aktif botun oturumları — botlar arası geçişte liste de o bota göre değişir.
486
540
  KATI KURAL: her oturum yalnız BAĞLI OLDUĞU botun listesinde görünür;
487
541
  bot kaydı olmayan (eski) oturumlar Beast (varsayılan sahip) altında görünür. */
488
- for (const s of list) {
542
+ for (const { s } of withRank) {
489
543
  if ((s.botId || 'beast') !== activeBotId) continue;
490
544
  const row = document.createElement('div');
491
545
  row.className = 'sess' + (s.id === activeId ? ' active' : '');
546
+ row.title = _t('sess_drag');
547
+ row.draggable = true;
492
548
  row.innerHTML =
493
549
  (waSet.has(s.id) ? '<span class="sess-wa" title="WhatsApp">W</span>' : '') +
494
550
  (tgSet.has(s.id) ? '<span class="sess-tg" title="Telegram">T</span>' : '') +
@@ -506,12 +562,40 @@ async function renderSessions(list) {
506
562
  const created = await beast.createSession();
507
563
  await openSession(created.id);
508
564
  }
565
+ sessionOrder = sessionOrder.filter((x) => x !== s.id);
509
566
  refreshSessions();
510
567
  });
568
+ /* sürükle-bırak: yukarı/aşağı yer değiştirme */
569
+ row.addEventListener('dragstart', (e) => {
570
+ dragSid = s.id;
571
+ row.classList.add('dragging');
572
+ try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', s.id); } catch {}
573
+ });
574
+ row.addEventListener('dragend', () => {
575
+ dragSid = null;
576
+ row.classList.remove('dragging');
577
+ els.sessList.querySelectorAll('.drag-over').forEach((x) => x.classList.remove('drag-over'));
578
+ });
579
+ row.addEventListener('dragover', (e) => {
580
+ if (!dragSid || dragSid === s.id) return;
581
+ e.preventDefault();
582
+ try { e.dataTransfer.dropEffect = 'move'; } catch {}
583
+ row.classList.add('drag-over');
584
+ });
585
+ row.addEventListener('dragleave', () => row.classList.remove('drag-over'));
586
+ row.addEventListener('drop', (e) => {
587
+ e.preventDefault();
588
+ row.classList.remove('drag-over');
589
+ if (!dragSid || dragSid === s.id) return;
590
+ reorderSessions(dragSid, s.id);
591
+ dragSid = null;
592
+ });
511
593
  els.sessList.appendChild(row);
512
594
  }
513
595
  }
514
596
 
597
+ let dragSid = null;
598
+
515
599
  async function refreshSessions() {
516
600
  await renderSessions(await beast.listSessions());
517
601
  renderBotCards(); // bot kartlarındaki numara/sayı etiketleri de tazelensin
@@ -749,6 +833,50 @@ async function refreshFalloutPane() {
749
833
  renderFalloutPane();
750
834
  }
751
835
 
836
+ /* Obscura kurulum ilerlemesi: main process'te sürer — ayarlardan çıkılsa da
837
+ kesilmez. Panel açıksa çubuk canlı güncellenir; bitişte tek kez toast atılır. */
838
+ let _obscuraWasRunning = false;
839
+ function paintObscuraState(st) {
840
+ const ocSt = $('#ocStatus');
841
+ const ocProg = $('#ocProg');
842
+ const ocBtn = $('#ocInstall');
843
+ if (!ocSt || !ocProg || !ocBtn || !st || !st.phase) return;
844
+ const pct = Math.max(0, Math.min(100, Number(st.pct) || 0));
845
+ const fill = ocProg.querySelector('.oc-prog-fill');
846
+ const txt = ocProg.querySelector('.oc-prog-text');
847
+ if (st.running) {
848
+ ocBtn.disabled = true;
849
+ ocBtn.textContent = _t('oc_install_running');
850
+ ocProg.hidden = false;
851
+ if (fill) fill.style.width = pct + '%';
852
+ const phaseKey = ({ 'hazırlanıyor': 'prep', 'indiriliyor': 'download', 'kuruluyor': 'extract', 'doğrulanıyor': 'verify' })[st.phase];
853
+ if (txt) txt.textContent = (phaseKey ? _t('oc_phase_' + phaseKey) : st.phase) + ' — ' + pct + '%';
854
+ ocSt.textContent = _t('oc_bg_note');
855
+ } else if (st.phase === 'tamamlandı' && pct >= 100) {
856
+ ocBtn.disabled = false;
857
+ ocBtn.textContent = _t('oc_install');
858
+ ocProg.hidden = false;
859
+ if (fill) fill.style.width = '100%';
860
+ if (txt) txt.textContent = _t('oc_phase_done') + ' — 100%';
861
+ ocSt.textContent = _t('oc_status_ok');
862
+ } else if (st.phase === 'hata') {
863
+ ocBtn.disabled = false;
864
+ ocBtn.textContent = _t('oc_install');
865
+ ocProg.hidden = true;
866
+ ocSt.textContent = _t('oc_install_fail') + (st.error || '?');
867
+ }
868
+ }
869
+
870
+ function onObscuraProgress(ev) {
871
+ if (ev && ev.running) _obscuraWasRunning = true;
872
+ paintObscuraState(ev);
873
+ if (ev && !ev.running && _obscuraWasRunning) {
874
+ _obscuraWasRunning = false;
875
+ if (ev.phase === 'tamamlandı') toast(_t('oc_installed_toast'));
876
+ else if (ev.phase === 'hata') toast(_t('oc_install_fail') + (ev.error || '?'));
877
+ }
878
+ }
879
+
752
880
  /* Web Arama sekmesi: TinyFish anahtarı + Obscura (kurulum/aktiflik) + arama sırası */
753
881
  async function renderWebSearchPane() {
754
882
  const pane = $('#tab-websearch');
@@ -764,7 +892,10 @@ async function renderWebSearchPane() {
764
892
  '<div id="ocStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
765
893
  '<label class="mem-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
766
894
  '<input id="ocEnabled" type="checkbox" style="width:auto" /> <span>' + _t('oc_enabled') + '</span></label>' +
767
- '<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:10px">' +
895
+ '<div id="ocProg" class="oc-prog" hidden>' +
896
+ '<div class="oc-prog-bar"><div class="oc-prog-fill" style="width:0%"></div></div>' +
897
+ '<div class="oc-prog-text sub"></div></div>' +
898
+ '<div style="display:flex;justify-content:center;margin-top:12px">' +
768
899
  '<button id="ocInstall" class="btn">' + _t('oc_install') + '</button></div>' +
769
900
  /* --- Arama sırası --- */
770
901
  '<div class="divider"></div>' +
@@ -786,22 +917,21 @@ async function renderWebSearchPane() {
786
917
  const ocSt = $('#ocStatus');
787
918
  const ocChk = $('#ocEnabled');
788
919
  const ocBtn = $('#ocInstall');
789
- const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '' }));
920
+ const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '', install: null }));
790
921
  ocChk.checked = oc.enabled !== false;
791
- ocSt.textContent = oc.installed ? _t('oc_status_ok') : _t('oc_status_missing');
922
+ /* kurulum arka planda sürüyorsa durum panelde GERİ YÜKLENİR —
923
+ ayarlardan çıkıp dönsen bile ilerleme kaybolmaz */
924
+ const ocState = oc.install || (await beast.obscuraInstallState().catch(() => null));
925
+ paintObscuraState(ocState);
792
926
  ocChk.addEventListener('change', async () => {
793
927
  await beast.obscuraSetEnabled(ocChk.checked).catch(() => {});
794
928
  toast(ocChk.checked ? _t('oc_on_toast') : _t('oc_off_toast'));
795
929
  });
796
930
  ocBtn.addEventListener('click', async () => {
797
- ocBtn.disabled = true;
798
- ocBtn.textContent = _t('oc_installing');
799
- ocSt.textContent = _t('oc_installing');
931
+ if (ocBtn.disabled) return;
800
932
  const r = await beast.obscuraInstall().catch(() => ({ ok: false, error: 'hata' }));
801
- ocBtn.disabled = false;
802
- ocBtn.textContent = _t('oc_install');
803
- ocSt.textContent = r && r.ok ? _t('oc_status_ok') : _t('oc_install_fail') + (r && r.error ? r.error : '?');
804
- toast(r && r.ok ? _t('oc_installed_toast') : _t('oc_install_fail') + (r && r.error ? r.error : '?'));
933
+ if (r && (r.ok || r.started || r.busy)) paintObscuraState(await beast.obscuraInstallState().catch(() => null));
934
+ else toast(_t('oc_install_fail') + ((r && r.error) || '?'));
805
935
  });
806
936
 
807
937
  /* --- Arama sırası --- */
@@ -3208,8 +3338,11 @@ function renderBotSettings(pane, b) {
3208
3338
  })
3209
3339
  );
3210
3340
  $('#bSave').addEventListener('click', async () => {
3341
+ const newName = $('#bName').value.trim();
3342
+ /* İLK HARF ZORUNLU: ad değiştirilirken de harf karakteriyle başlamalı */
3343
+ if (newName && !/^\p{L}/u.test(newName)) { toast(_t('bot_name_letter')); $('#bName').focus(); return; }
3211
3344
  const patch = {
3212
- name: $('#bName').value.trim(),
3345
+ name: newName,
3213
3346
  icon: (f.querySelector('#bIconPick button.on') || {}).dataset?.ic || b.icon,
3214
3347
  prompt: $('#bPrompt').value,
3215
3348
  seeBots: [...f.querySelectorAll('#bSee input:checked')].map((c) => c.dataset.see),
@@ -3235,8 +3368,12 @@ function renderBotSettings(pane, b) {
3235
3368
  if (delBtn) delBtn.addEventListener('click', async () => {
3236
3369
  if (!confirm(_ti('bot_confirm_del', b.name))) return;
3237
3370
  const r = await beast.botsRemove(b.id);
3238
- if (r.ok) { toast(_t('bot_deleted')); botPageId = null; await refreshBots(); renderBotPage(); }
3239
- else toast(r.error || _t('bot_save_fail'));
3371
+ if (r.ok) {
3372
+ /* bot silme sonrası main otomatik restart atar — yeniden çizmeye kalkışma */
3373
+ toast(r.restarting ? _t('bot_deleted_restart') : _t('bot_deleted'));
3374
+ botPageId = null;
3375
+ if (!r.restarting) { await refreshBots(); renderBotPage(); }
3376
+ } else toast(r.error || _t('bot_save_fail'));
3240
3377
  });
3241
3378
  /* (numara ekleme alanı kaldırıldı — Entegrasyonlar üzerinden yapılır) */
3242
3379
  }
@@ -3358,6 +3495,8 @@ function renderBotAdd(pane) {
3358
3495
  $('#bAddCreate').addEventListener('click', async () => {
3359
3496
  const name = $('#bAddName').value.trim();
3360
3497
  if (!name) { toast(_t('bot_name_req')); $('#bAddName').focus(); return; }
3498
+ /* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
3499
+ if (!/^\p{L}/u.test(name)) { toast(_t('bot_name_letter')); $('#bAddName').focus(); return; }
3361
3500
  const icon = (f.querySelector('#bAddIconPick button.on') || {}).dataset?.ic || '🤖';
3362
3501
  const r = await beast.botsAdd({ name, icon, prompt: $('#bAddPrompt').value });
3363
3502
  if (r.ok) {
@@ -3715,6 +3854,8 @@ function onEvent(ev) {
3715
3854
  }
3716
3855
  /* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
3717
3856
  if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
3857
+ /* Obscura kurulum ilerlemesi — ayarlardan çıkıp dönülsen de main'de sürer */
3858
+ if (ev.type === 'obscura-progress') { onObscuraProgress(ev); return; }
3718
3859
  /* Beast Code oturumu (IDE modu ortasındaki panel): olayları panele akıt,
3719
3860
  ana sohbeti kirletme */
3720
3861
  if (bcSessionId && ev.sessionId === bcSessionId) {
@@ -4301,6 +4442,7 @@ function autosize() {
4301
4442
 
4302
4443
  const SLASH_COMMANDS = [
4303
4444
  { cmd: '/help', desc: 'tüm komutları listele' },
4445
+ { cmd: '/version', desc: 'Beast Agent sürümünü göster' },
4304
4446
  { cmd: '/new', desc: 'yeni oturum aç (kod verilir)' },
4305
4447
  { cmd: '/open ', desc: 'koddaki oturuma geç' },
4306
4448
  { cmd: '/sessions', desc: 'bu sohbetin oturumları' },
@@ -6166,6 +6308,15 @@ function bcIngest(ev) {
6166
6308
  case 'tool-end':
6167
6309
  bcToolBoxEnd(ev.callId, ev.ok, ev.result);
6168
6310
  break;
6311
+ case 'bc-mode':
6312
+ /* OpenCode disiplini çalışma modu: başlıkta rozet + panelde bilgi satırı */
6313
+ if (els.bcTitle) {
6314
+ els.bcTitle.textContent =
6315
+ ev.mode === 'plan' ? 'BEAST CODE · PLAN' :
6316
+ ev.mode === 'build' ? 'BEAST CODE · BUILD' : 'BEAST CODE';
6317
+ }
6318
+ bcLine('t-dim', '[' + (ev.body || ev.mode || 'mod değişti') + ']');
6319
+ break;
6169
6320
  case 'done':
6170
6321
  bcFlushStream();
6171
6322
  bcCloseToolGroup();
@@ -6203,7 +6354,12 @@ function bcRunCurrent() {
6203
6354
  /* sürüyor/serbest kararını main'deki gerçek engine.isBusy verir —
6204
6355
  panel bayat kilitli kaldıysa ilk mesajla kendini düzeltir */
6205
6356
  beast.beastcodeSend(msg).then((r) => {
6206
- if (r && r.ok) {
6357
+ if (r && r.ok && r.mode) {
6358
+ /* /plan · /build · /auto: mod değişimi — tur AÇILMAZ, kilit anında açılır
6359
+ (bilgi satırı bc-mode olayıyla düşer, başlık rozeti güncellenir) */
6360
+ bcSessionId = r.sessionId;
6361
+ bcSetBusy(false);
6362
+ } else if (r && r.ok) {
6207
6363
  bcSessionId = r.sessionId;
6208
6364
  } else if (r && r.busy) {
6209
6365
  /* iş gerçekten sürüyor — kilit açılmaz, bilgi satırı düşer */
@@ -6238,6 +6394,7 @@ if (els.bcNew) els.bcNew.addEventListener('click', async () => {
6238
6394
  bcCloseToolGroup();
6239
6395
  bcHideTodos();
6240
6396
  bcFlushStream();
6397
+ if (els.bcTitle) els.bcTitle.textContent = 'BEAST CODE';
6241
6398
  bcLine('t-sys', 'yeni oturum — sonraki mesaj taze başlar');
6242
6399
  }
6243
6400
  });
@@ -119,6 +119,9 @@ body {
119
119
  }
120
120
  .sess:hover { background: var(--panel2); color: var(--text); }
121
121
  .sess.active { background: var(--panel2); color: var(--text); box-shadow: inset 2px 0 0 var(--accent); }
122
+ /* sürükle-bırak sıralama görselleri */
123
+ .sess.dragging { opacity: 0.35; }
124
+ .sess.drag-over { box-shadow: inset 0 2px 0 var(--accent); }
122
125
  .sess-wa {
123
126
  width: 15px;
124
127
  height: 15px;
@@ -291,6 +294,13 @@ body.browser-open #topbar { width: auto; }
291
294
  #eyeBtn:not(.on) { opacity: 0.55; }
292
295
  #eyeBtn.on { background: var(--accent-dim); color: var(--accent); }
293
296
 
297
+ /* internet göstergesi (topbar sağ uç): bağlı = YEŞİL wifi, kopuk = KIRMIZI */
298
+ #netDot { cursor: default; }
299
+ #netDot.net-on { color: #22c55e; }
300
+ #netDot.net-off { color: #ef4444; }
301
+ #netDot.net-off .net-slash { display: block; }
302
+ #netDot.net-on .net-slash { display: none; }
303
+
294
304
  /* #19 sağ panel (paralel ajan konsolu) aç/kapa */
295
305
  #railBtn.on { background: var(--accent-dim); color: var(--accent); }
296
306
  body.rail-hidden #rail { display: none; }
@@ -770,6 +780,23 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
770
780
  word-break: break-word;
771
781
  }
772
782
 
783
+ /* Obscura kurulum ilerleme çubuğu (Ayarlar → Web Arama) */
784
+ .oc-prog { margin-top: 10px; }
785
+ .oc-prog-bar {
786
+ height: 9px;
787
+ background: var(--accent-dim);
788
+ border: 1px solid var(--border);
789
+ border-radius: 6px;
790
+ overflow: hidden;
791
+ }
792
+ .oc-prog-fill {
793
+ height: 100%;
794
+ width: 0%;
795
+ background: var(--accent);
796
+ transition: width 0.25s ease;
797
+ }
798
+ .oc-prog-text { margin-top: 5px; font-size: 12px; color: var(--muted); }
799
+
773
800
  /* markdown */
774
801
  .md p { margin: 0 0 8px; }
775
802
  .md p:last-child { margin-bottom: 0; }
@@ -910,6 +937,18 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
910
937
  margin-bottom: 5px;
911
938
  }
912
939
  #todoPanel .td-count { letter-spacing: 0; font-weight: 700; color: var(--text); }
940
+ #todoPanel .td-right { display: flex; align-items: center; gap: 8px; }
941
+ /* sağ köşe kapatma (×): yalnız bu listeyi gizler — yeni güncellemede yine görünür */
942
+ #todoPanel .td-x {
943
+ border: none;
944
+ background: none;
945
+ color: var(--muted);
946
+ font-size: 14px;
947
+ line-height: 1;
948
+ padding: 0 3px;
949
+ cursor: pointer;
950
+ }
951
+ #todoPanel .td-x:hover { color: var(--err); }
913
952
  #todoPanel .td-row {
914
953
  display: flex;
915
954
  align-items: baseline;
@@ -1784,14 +1823,16 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1784
1823
  flex-shrink: 0;
1785
1824
  }
1786
1825
 
1787
- /* tek tıkla model (OpenCode Zen) hero butonu */
1826
+ /* tek tıkla model (OpenCode Zen) hero butonu — buton ortada */
1788
1827
  .zen-hero {
1789
1828
  padding: 10px 12px;
1790
1829
  border: 1px dashed var(--border);
1791
1830
  border-radius: 10px;
1792
1831
  margin-bottom: 12px;
1793
1832
  background: var(--panel);
1833
+ text-align: center;
1794
1834
  }
1835
+ .zen-hero .sub { text-align: left; }
1795
1836
  .zen-hero .btn { min-width: 220px; }
1796
1837
  .zen-hero .btn .zen-load {
1797
1838
  display: inline-block;
@@ -286,6 +286,51 @@ test('#5 eşzamanlılık limiti: fazlası kuyrukta bekler, slot boşalınca FIFO
286
286
 
287
287
  /* ---------- İPTAL SEBEBİ DİSİPLİNİ ---------- */
288
288
 
289
+ test('üst oturum hata verirse rapor TEK SEFER düşer — sonsuz _run zinciri yok', async () => {
290
+ /* model yok → bg işi error ile biter → rapor üst oturuma flush edilir;
291
+ üst oturum da model-yok ile SYNC çökerse flush zinciri SONSUZA dönmemeli
292
+ (gerçek hata: flush → send → _run sync-throw → finally → flush … stack overflow) */
293
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-flush-'));
294
+ let runCount = 0;
295
+ const eng = new Engine({}, { sessionsDir: dir, emit: () => {} });
296
+ const origRun = Engine.prototype._run;
297
+ eng._run = function (s) { runCount++; return origRun.call(this, s); };
298
+ eng.runBackground('pflush', 'iş', 'Rapor İş');
299
+ await sleep(700);
300
+ assert.ok(runCount <= 4, 'sonsuz _run zinciri yok (runCount=' + runCount + ')');
301
+ /* rapor tam olarak 1 kez üst oturuma gönderilmiş olmalı */
302
+ eng.deleteSession('pflush');
303
+ });
304
+
305
+ test('takılı iş ZORLA kapatılır: grup tamamlanır, birleşik rapor düşer', async () => {
306
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-fc-'));
307
+ const eng = new Engine({}, { sessionsDir: dir, emit: () => {} });
308
+ const sent = [];
309
+ eng.send = (sid, payload) => { sent.push({ sid, text: String((payload && payload.text) || '') }); return true; };
310
+ const r = eng.runBackgroundMany(
311
+ 'pfc',
312
+ [
313
+ { title: 'A1', task: 'görev a' },
314
+ { title: 'B1', task: 'görev b' },
315
+ ],
316
+ 'Zorla Grup'
317
+ );
318
+ assert.ok(r.ok);
319
+ /* A1 done; B1 takılı → force-close */
320
+ await eng.reportBackgroundDone(r.ids[0]);
321
+ const stuck = eng._bgJobs.get(r.ids[1]);
322
+ stuck.fixes = 99; /* öz-kurtarma hakları tükenmiş */
323
+ eng._bgForceClose(stuck, 'test: takıldı');
324
+ await sleep(30);
325
+ const j = eng._bgJobs.get(r.ids[1]);
326
+ assert.strictEqual(j.status, 'aborted', 'zorla kapatılan iş aborted');
327
+ assert.ok(String(j.error || '').includes('test: takıldı'), 'sebep kayda geçti');
328
+ const rep = sent.find((x) => x.sid === 'pfc' && x.text.includes('[ARKA PLAN GRUP BİTTİ'));
329
+ assert.ok(rep, 'birleşik rapor düştü — grup kilitlenmedi');
330
+ assert.ok(/B1/.test(rep.text) && /aborted/.test(rep.text), 'raporda takılan üye SEBEPİYLE görünüyor');
331
+ eng.deleteSession('pfc');
332
+ });
333
+
289
334
  test('iptal SEBEBİ zorunlu: interrupt sebebi iş kaydına ve üst sohbete yazar', async () => {
290
335
  const { eng } = tmpEngine();
291
336
  const sent = [];
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ /* Bot adı disiplini: İLK KARAKTER HARF ZORUNLU (ekleme + güncelleme) */
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+
11
+ /* BEAST_DATA modül yüklenmeden önce ayarlanmalı (REG cache'i tek dosyada) */
12
+ process.env.BEAST_DATA = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-bots-name-'));
13
+ const bots = require('../src/agent/bots');
14
+
15
+ test('bot ekleme: ad harf ile başlamalı', () => {
16
+ for (const bad of ['1Muhasebe', '9bot', '-abc', '_x', ' 3BoT']) {
17
+ const r = bots.add({ name: bad, prompt: '' });
18
+ assert.strictEqual(r.ok, false, bad + ' reddedilmeli');
19
+ assert.ok(/harf ile başlamalı/.test(r.error), r.error);
20
+ }
21
+ const okNum = bots.add({ name: 'Muhasebe', prompt: '' });
22
+ assert.ok(okNum.ok, 'harf ile başlayan ad kabul: ' + (okNum.error || ''));
23
+ const okTr = bots.add({ name: 'Çağrı', prompt: '' });
24
+ assert.ok(okTr.ok, 'Türkçe harf (Ç) kabul: ' + (okTr.error || ''));
25
+ });
26
+
27
+ test('bot güncelleme: geçersiz ad diğer alanlara dokunmadan reddedilir', () => {
28
+ const created = bots.add({ name: 'Depo', prompt: 'eski' });
29
+ assert.ok(created.ok);
30
+ const id = created.bot.id;
31
+
32
+ const bad = bots.update(id, { name: '3Depo', prompt: 'yeni' });
33
+ assert.strictEqual(bad.ok, false, 'geçersiz ad reddedildi');
34
+ const after = bots.get(id);
35
+ assert.strictEqual(after.name, 'Depo', 'eski ad korundu');
36
+ assert.strictEqual(after.prompt, 'eski', 'prompt değişmedi');
37
+
38
+ const good = bots.update(id, { name: 'Anbar2', prompt: 'yeni' });
39
+ assert.ok(good.ok, 'harf ile başlayan güncelleme kabul: ' + (good.error || ''));
40
+ assert.strictEqual(bots.get(id).prompt, 'yeni');
41
+ assert.strictEqual(bots.get(id).name, 'Anbar2');
42
+ });
package/tests/llm.test.js CHANGED
@@ -3,7 +3,7 @@
3
3
  require('./setup');
4
4
  const test = require('node:test');
5
5
  const assert = require('node:assert');
6
- const { withRetries, friendlyError, sumUsage, chatStreamAuto } = require('../src/agent/llm');
6
+ const { withRetries, friendlyError, sumUsage, chatStreamAuto, setNetProbe, isNetworkError } = require('../src/agent/llm');
7
7
 
8
8
  test('withRetries: 503 sonrası başarılı isteği tekrarlar', async () => {
9
9
  let calls = 0;
@@ -134,3 +134,72 @@ test('chatStreamAuto: normal (stop) yanıtta hiç devam turu açılmaz', async (
134
134
  srv.close();
135
135
  }
136
136
  });
137
+
138
+ /* ---------- ağ kopması dayanıklılığı (#retry) ---------- */
139
+
140
+ test('isNetworkError sınıflandırması', () => {
141
+ const net = new Error('fetch failed');
142
+ assert.equal(isNetworkError(net), true, 'status yok → ağ hatası');
143
+ const http = new Error('HTTP 503');
144
+ http.status = 503;
145
+ assert.equal(isNetworkError(http), false, 'status var → HTTP hatası');
146
+ const abort = new Error('iptal');
147
+ abort.name = 'AbortError';
148
+ assert.equal(isNetworkError(abort), false, 'abort ağ hatası sayılmaz');
149
+ assert.equal(isNetworkError(null), false);
150
+ });
151
+
152
+ test('withRetries: internet kapalıyken probe bekler, dönünce başarır', async () => {
153
+ let online = false;
154
+ setNetProbe(() => online);
155
+ setTimeout(() => { online = true; }, 30);
156
+ let calls = 0;
157
+ const t0 = Date.now();
158
+ const r = await withRetries(async () => {
159
+ calls++;
160
+ if (calls === 1) throw new Error('fetch failed');
161
+ return 'ok';
162
+ });
163
+ assert.equal(r, 'ok');
164
+ assert.equal(calls, 2);
165
+ assert.ok(Date.now() - t0 > 2000, 'probe çevrimdışı dediği için bekledi');
166
+ setNetProbe(null);
167
+ });
168
+
169
+ test('akış ortasında bağlantı koparsa: kısmi metinle devam turu açılır (hata balonu yok)', async () => {
170
+ const http = require('node:http');
171
+ const calls = [];
172
+ const srv = http.createServer((req, res) => {
173
+ let body = '';
174
+ req.on('data', (c) => (body += c));
175
+ req.on('end', () => {
176
+ calls.push(JSON.parse(body).messages);
177
+ if (calls.length === 1) {
178
+ /* birkaç delta yaz, sonra soketi ORTADAN kopar */
179
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
180
+ res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba du' } }] }) + '\n\n');
181
+ setTimeout(() => res.destroy(), 30);
182
+ return;
183
+ }
184
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
185
+ res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'nya devam' } }] }) + '\n\n');
186
+ res.write('data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n');
187
+ res.write('data: [DONE]\n\n');
188
+ res.end();
189
+ });
190
+ });
191
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
192
+ try {
193
+ const port = srv.address().port;
194
+ const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
195
+ const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
196
+ assert.equal(r.content, 'Merhaba dunya devam', 'kısmi + devam birleşti');
197
+ assert.equal(r.finishReason, 'stop');
198
+ assert.equal(calls.length, 2, 'devam turu açıldı');
199
+ const m2 = calls[1];
200
+ assert.equal(m2[m2.length - 2].role, 'assistant');
201
+ assert.match(m2[m2.length - 1].content, /DEVAM/);
202
+ } finally {
203
+ srv.close();
204
+ }
205
+ });