beast-agent 0.27.0 → 0.29.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'));
@@ -383,6 +393,17 @@ function addErrorBubble(text) {
383
393
  scrollDown(true);
384
394
  }
385
395
 
396
+ /* ajan durduruldu/iptal edildi — SEBEP zorunlu, sohbete sönük not olarak düşer */
397
+ function addStopNote(reason) {
398
+ streamEl = null;
399
+ showEmpty(false);
400
+ const div = document.createElement('div');
401
+ div.className = 'msg msg-sys';
402
+ div.textContent = '\u23F9 Durduruldu — sebep: ' + String(reason || 'sebep belirtilmedi');
403
+ els.msgs.appendChild(div);
404
+ scrollDown(true);
405
+ }
406
+
386
407
  /* ---------------- tool cards ---------------- */
387
408
  /* Ard arda gelen araç kartları TEK kutuda toplanır (.tool-box): çalışırken
388
409
  lacivert outline döner, gövde max 420px scroll'lu. Grup; tool_calls İÇEREN
@@ -738,12 +759,77 @@ async function refreshFalloutPane() {
738
759
  renderFalloutPane();
739
760
  }
740
761
 
741
- /* Web Arama sekmesi: TinyFish (zincirin başı) + Exa API anahtarı (maskeli) */
762
+ /* Obscura kurulum ilerlemesi: main process'te sürer — ayarlardan çıkılsa da
763
+ kesilmez. Panel açıksa çubuk canlı güncellenir; bitişte tek kez toast atılır. */
764
+ let _obscuraWasRunning = false;
765
+ function paintObscuraState(st) {
766
+ const ocSt = $('#ocStatus');
767
+ const ocProg = $('#ocProg');
768
+ const ocBtn = $('#ocInstall');
769
+ if (!ocSt || !ocProg || !ocBtn || !st || !st.phase) return;
770
+ const pct = Math.max(0, Math.min(100, Number(st.pct) || 0));
771
+ const fill = ocProg.querySelector('.oc-prog-fill');
772
+ const txt = ocProg.querySelector('.oc-prog-text');
773
+ if (st.running) {
774
+ ocBtn.disabled = true;
775
+ ocBtn.textContent = _t('oc_install_running');
776
+ ocProg.hidden = false;
777
+ if (fill) fill.style.width = pct + '%';
778
+ const phaseKey = ({ 'hazırlanıyor': 'prep', 'indiriliyor': 'download', 'kuruluyor': 'extract', 'doğrulanıyor': 'verify' })[st.phase];
779
+ if (txt) txt.textContent = (phaseKey ? _t('oc_phase_' + phaseKey) : st.phase) + ' — ' + pct + '%';
780
+ ocSt.textContent = _t('oc_bg_note');
781
+ } else if (st.phase === 'tamamlandı' && pct >= 100) {
782
+ ocBtn.disabled = false;
783
+ ocBtn.textContent = _t('oc_install');
784
+ ocProg.hidden = false;
785
+ if (fill) fill.style.width = '100%';
786
+ if (txt) txt.textContent = _t('oc_phase_done') + ' — 100%';
787
+ ocSt.textContent = _t('oc_status_ok');
788
+ } else if (st.phase === 'hata') {
789
+ ocBtn.disabled = false;
790
+ ocBtn.textContent = _t('oc_install');
791
+ ocProg.hidden = true;
792
+ ocSt.textContent = _t('oc_install_fail') + (st.error || '?');
793
+ }
794
+ }
795
+
796
+ function onObscuraProgress(ev) {
797
+ if (ev && ev.running) _obscuraWasRunning = true;
798
+ paintObscuraState(ev);
799
+ if (ev && !ev.running && _obscuraWasRunning) {
800
+ _obscuraWasRunning = false;
801
+ if (ev.phase === 'tamamlandı') toast(_t('oc_installed_toast'));
802
+ else if (ev.phase === 'hata') toast(_t('oc_install_fail') + (ev.error || '?'));
803
+ }
804
+ }
805
+
806
+ /* Web Arama sekmesi: TinyFish anahtarı + Obscura (kurulum/aktiflik) + arama sırası */
742
807
  async function renderWebSearchPane() {
743
808
  const pane = $('#tab-websearch');
744
809
  if (!pane) return;
745
810
  const tf = await beast.tinyfishGet().catch(() => ({ set: false, masked: '' }));
746
811
  pane.innerHTML =
812
+ '<h2>' + _t('ws_h2') + '</h2>' +
813
+ '<div class="sub">' + _t('ws_sub') + '</div>' +
814
+ /* --- Obscura --- */
815
+ '<div class="divider"></div>' +
816
+ '<h2>' + _t('oc_h2') + '</h2>' +
817
+ '<div class="sub">' + _t('oc_sub') + '</div>' +
818
+ '<div id="ocStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
819
+ '<label class="mem-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
820
+ '<input id="ocEnabled" type="checkbox" style="width:auto" /> <span>' + _t('oc_enabled') + '</span></label>' +
821
+ '<div id="ocProg" class="oc-prog" hidden>' +
822
+ '<div class="oc-prog-bar"><div class="oc-prog-fill" style="width:0%"></div></div>' +
823
+ '<div class="oc-prog-text sub"></div></div>' +
824
+ '<div style="display:flex;justify-content:center;margin-top:12px">' +
825
+ '<button id="ocInstall" class="btn">' + _t('oc_install') + '</button></div>' +
826
+ /* --- Arama sırası --- */
827
+ '<div class="divider"></div>' +
828
+ '<h2>' + _t('so_h2') + '</h2>' +
829
+ '<div class="sub">' + _t('so_sub') + '</div>' +
830
+ '<div id="soList" style="margin-top:10px"></div>' +
831
+ /* --- TinyFish --- */
832
+ '<div class="divider"></div>' +
747
833
  '<h2>' + _t('tf_h2') + '</h2>' +
748
834
  '<div class="sub">' + _t('tf_sub') + '</div>' +
749
835
  '<div id="tfStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
@@ -751,26 +837,79 @@ async function renderWebSearchPane() {
751
837
  '<input id="tfKeyInp" class="inp" type="password" placeholder="tf_..." autocomplete="new-password" spellcheck="false" />' +
752
838
  '<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
753
839
  '<button id="tfSave" class="btn">' + _t('ws_save') + '</button>' +
754
- '<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>' +
755
- '<div class="divider"></div>' +
756
- '<h2>' + _t('ws_h2') + '</h2>' +
757
- '<div class="sub">' + _t('ws_sub') + '</div>' +
758
- '<div id="exaStatus" class="sub" style="text-align:left;margin-top:10px"></div>' +
759
- '<label class="mem-label">' + _t('ws_exa_label') + '</label>' +
760
- '<input id="exaKeyInp" class="inp" type="password" placeholder="' + _t('ws_exa_ph') + '" autocomplete="new-password" spellcheck="false" />' +
761
- '<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
762
- '<button id="exaSave" class="btn">' + _t('ws_save') + '</button>' +
763
- '<button id="exaClear" class="btn ghost">' + _t('ws_clear') + '</button></div>' +
764
- '<div class="sub" style="margin-top:8px">' + _t('ws_note') + '</div>';
765
- const st = $('#exaStatus');
766
- const setExaSt = (r) => {
767
- st.textContent = r.set ? _t('ws_status_set') + r.masked : _t('ws_status_unset');
768
- };
840
+ '<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>';
841
+
842
+ /* --- Obscura durumu --- */
843
+ const ocSt = $('#ocStatus');
844
+ const ocChk = $('#ocEnabled');
845
+ const ocBtn = $('#ocInstall');
846
+ const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '', install: null }));
847
+ ocChk.checked = oc.enabled !== false;
848
+ /* kurulum arka planda sürüyorsa durum panelde GERİ YÜKLENİR —
849
+ ayarlardan çıkıp dönsen bile ilerleme kaybolmaz */
850
+ const ocState = oc.install || (await beast.obscuraInstallState().catch(() => null));
851
+ paintObscuraState(ocState);
852
+ ocChk.addEventListener('change', async () => {
853
+ await beast.obscuraSetEnabled(ocChk.checked).catch(() => {});
854
+ toast(ocChk.checked ? _t('oc_on_toast') : _t('oc_off_toast'));
855
+ });
856
+ ocBtn.addEventListener('click', async () => {
857
+ if (ocBtn.disabled) return;
858
+ const r = await beast.obscuraInstall().catch(() => ({ ok: false, error: 'hata' }));
859
+ if (r && (r.ok || r.started || r.busy)) paintObscuraState(await beast.obscuraInstallState().catch(() => null));
860
+ else toast(_t('oc_install_fail') + ((r && r.error) || '?'));
861
+ });
862
+
863
+ /* --- Arama sırası --- */
864
+ const soBox = $('#soList');
865
+ let rows = [];
866
+ const soRes = await beast.searchOrderGet().catch(() => ({ chain: [] }));
867
+ rows = (soRes && Array.isArray(soRes.chain) && soRes.chain.length) ? soRes.chain.map((x) => ({ id: x.id, on: x.on !== false })) : [];
868
+ const engName = (id) => _t('so_engine_' + id) || id;
869
+ function renderRows() {
870
+ soBox.innerHTML = '';
871
+ rows.forEach((row, i) => {
872
+ const el = document.createElement('div');
873
+ el.className = 'so-row' + (row.on ? '' : ' off');
874
+ el.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px';
875
+ el.innerHTML =
876
+ '<span class="so-idx" style="min-width:18px;color:var(--muted);font-size:12px">' + (i + 1) + '.</span>' +
877
+ '<input type="checkbox" class="so-chk" style="width:auto" ' + (row.on ? 'checked' : '') + ' />' +
878
+ '<span style="flex:1">' + escapeHtml(engName(row.id)) + '</span>' +
879
+ '<button class="so-up" title="Yukarı" style="width:auto;padding:2px 8px">↑</button>' +
880
+ '<button class="so-down" title="Aşağı" style="width:auto;padding:2px 8px">↓</button>';
881
+ el.querySelector('.so-chk').addEventListener('change', async (e) => {
882
+ row.on = e.target.checked;
883
+ el.classList.toggle('off', !row.on);
884
+ await saveOrder();
885
+ });
886
+ el.querySelector('.so-up').addEventListener('click', async () => {
887
+ if (i === 0) return;
888
+ [rows[i - 1], rows[i]] = [rows[i], rows[i - 1]];
889
+ renderRows();
890
+ await saveOrder();
891
+ });
892
+ el.querySelector('.so-down').addEventListener('click', async () => {
893
+ if (i >= rows.length - 1) return;
894
+ [rows[i + 1], rows[i]] = [rows[i], rows[i + 1]];
895
+ renderRows();
896
+ await saveOrder();
897
+ });
898
+ soBox.appendChild(el);
899
+ });
900
+ }
901
+ async function saveOrder() {
902
+ const r = await beast.searchOrderSet(rows).catch(() => null);
903
+ if (r && r.ok) toast(_t('so_saved_toast'));
904
+ else toast(_t('ws_fail_toast'));
905
+ }
906
+ if (rows.length) renderRows();
907
+
908
+ /* --- TinyFish --- */
769
909
  const tfSt = $('#tfStatus');
770
910
  const setTfSt = (r) => {
771
911
  tfSt.textContent = r.set ? _t('tf_status_set') + r.masked : _t('tf_status_unset');
772
912
  };
773
- setExaSt(await beast.exaGet().catch(() => ({ set: false, masked: '' })));
774
913
  setTfSt(tf);
775
914
  $('#tfSave').addEventListener('click', async () => {
776
915
  const v = $('#tfKeyInp').value.trim();
@@ -790,24 +929,6 @@ async function renderWebSearchPane() {
790
929
  setTfSt({ set: false, masked: '' });
791
930
  toast(_t('tf_cleared_toast'));
792
931
  });
793
- $('#exaSave').addEventListener('click', async () => {
794
- const v = $('#exaKeyInp').value.trim();
795
- if (!v) { toast(_t('ws_empty_toast')); return; }
796
- const rr = await beast.exaSet(v).catch(() => null);
797
- $('#exaKeyInp').value = '';
798
- if (rr && rr.set) {
799
- setExaSt(rr);
800
- toast(_t('ws_saved_toast'));
801
- } else {
802
- toast(_t('ws_fail_toast'));
803
- }
804
- });
805
- $('#exaClear').addEventListener('click', async () => {
806
- await beast.exaClear().catch(() => {});
807
- $('#exaKeyInp').value = '';
808
- setExaSt({ set: false, masked: '' });
809
- toast(_t('ws_cleared_toast'));
810
- });
811
932
  }
812
933
 
813
934
  async function renderProviderPane() {
@@ -3143,8 +3264,11 @@ function renderBotSettings(pane, b) {
3143
3264
  })
3144
3265
  );
3145
3266
  $('#bSave').addEventListener('click', async () => {
3267
+ const newName = $('#bName').value.trim();
3268
+ /* İLK HARF ZORUNLU: ad değiştirilirken de harf karakteriyle başlamalı */
3269
+ if (newName && !/^\p{L}/u.test(newName)) { toast(_t('bot_name_letter')); $('#bName').focus(); return; }
3146
3270
  const patch = {
3147
- name: $('#bName').value.trim(),
3271
+ name: newName,
3148
3272
  icon: (f.querySelector('#bIconPick button.on') || {}).dataset?.ic || b.icon,
3149
3273
  prompt: $('#bPrompt').value,
3150
3274
  seeBots: [...f.querySelectorAll('#bSee input:checked')].map((c) => c.dataset.see),
@@ -3170,8 +3294,12 @@ function renderBotSettings(pane, b) {
3170
3294
  if (delBtn) delBtn.addEventListener('click', async () => {
3171
3295
  if (!confirm(_ti('bot_confirm_del', b.name))) return;
3172
3296
  const r = await beast.botsRemove(b.id);
3173
- if (r.ok) { toast(_t('bot_deleted')); botPageId = null; await refreshBots(); renderBotPage(); }
3174
- else toast(r.error || _t('bot_save_fail'));
3297
+ if (r.ok) {
3298
+ /* bot silme sonrası main otomatik restart atar — yeniden çizmeye kalkışma */
3299
+ toast(r.restarting ? _t('bot_deleted_restart') : _t('bot_deleted'));
3300
+ botPageId = null;
3301
+ if (!r.restarting) { await refreshBots(); renderBotPage(); }
3302
+ } else toast(r.error || _t('bot_save_fail'));
3175
3303
  });
3176
3304
  /* (numara ekleme alanı kaldırıldı — Entegrasyonlar üzerinden yapılır) */
3177
3305
  }
@@ -3293,6 +3421,8 @@ function renderBotAdd(pane) {
3293
3421
  $('#bAddCreate').addEventListener('click', async () => {
3294
3422
  const name = $('#bAddName').value.trim();
3295
3423
  if (!name) { toast(_t('bot_name_req')); $('#bAddName').focus(); return; }
3424
+ /* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
3425
+ if (!/^\p{L}/u.test(name)) { toast(_t('bot_name_letter')); $('#bAddName').focus(); return; }
3296
3426
  const icon = (f.querySelector('#bAddIconPick button.on') || {}).dataset?.ic || '🤖';
3297
3427
  const r = await beast.botsAdd({ name, icon, prompt: $('#bAddPrompt').value });
3298
3428
  if (r.ok) {
@@ -3447,7 +3577,7 @@ function renderAgentRail() {
3447
3577
  `<span class="ag-dot"></span>` +
3448
3578
  `<span class="sess-title">${escapeHtml(j.title)}</span>` +
3449
3579
  (j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
3450
- `<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
3580
+ `<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status) + (j.status === 'aborted' && j.error ? ' — ' + escapeHtml(String(j.error).slice(0, 70)) : ''))}</span>` +
3451
3581
  (j.status === 'running'
3452
3582
  ? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
3453
3583
  : `<button class="rj-cancel" title="${_t('ag_delete')}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>`) +
@@ -3577,7 +3707,7 @@ function renderAgentsPane() {
3577
3707
  `<div class="ag-head">` +
3578
3708
  `<span class="ag-dot"></span>` +
3579
3709
  `<span class="ag-title">${escapeHtml(j.title)}</span>` +
3580
- `<span class="ag-st">${agStText(j.status) || j.status}</span>` +
3710
+ `<span class="ag-st" title="${j.error ? escapeHtml(String(j.error)) : ''}">${agStText(j.status) || j.status}</span>` +
3581
3711
  `<span class="ag-time">${when}${j.status === 'running' ? ' · ' + agentTimerHtml(j.startedAt) : j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
3582
3712
  (j.status === 'running'
3583
3713
  ? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
@@ -3650,6 +3780,8 @@ function onEvent(ev) {
3650
3780
  }
3651
3781
  /* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
3652
3782
  if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
3783
+ /* Obscura kurulum ilerlemesi — ayarlardan çıkıp dönülsen de main'de sürer */
3784
+ if (ev.type === 'obscura-progress') { onObscuraProgress(ev); return; }
3653
3785
  /* Beast Code oturumu (IDE modu ortasındaki panel): olayları panele akıt,
3654
3786
  ana sohbeti kirletme */
3655
3787
  if (bcSessionId && ev.sessionId === bcSessionId) {
@@ -3764,6 +3896,8 @@ function onEvent(ev) {
3764
3896
  break;
3765
3897
  case 'done':
3766
3898
  closeChatToolGroup();
3899
+ /* iptal/durdurma sebebini SOHBETE yaz — sebepsiz durdurma yok */
3900
+ if (ev.aborted) addStopNote(ev.reason);
3767
3901
  setBusy(false);
3768
3902
  setStatus(''); /* son durum balonu ("düşünüyor…" vb.) yapışmasın */
3769
3903
  /* cevabın SONU görünsün: markdown son render sonrası iki kez en alta kilitle */
@@ -4234,6 +4368,7 @@ function autosize() {
4234
4368
 
4235
4369
  const SLASH_COMMANDS = [
4236
4370
  { cmd: '/help', desc: 'tüm komutları listele' },
4371
+ { cmd: '/version', desc: 'Beast Agent sürümünü göster' },
4237
4372
  { cmd: '/new', desc: 'yeni oturum aç (kod verilir)' },
4238
4373
  { cmd: '/open ', desc: 'koddaki oturuma geç' },
4239
4374
  { cmd: '/sessions', desc: 'bu sohbetin oturumları' },
@@ -6103,7 +6238,8 @@ function bcIngest(ev) {
6103
6238
  bcFlushStream();
6104
6239
  bcCloseToolGroup();
6105
6240
  bcSetBusy(false);
6106
- bcLine('t-dim', '(tamamlandı)');
6241
+ /* iptal/durdurma SEBEBİ panelde de görünür */
6242
+ bcLine(ev.aborted ? 't-err' : 't-dim', ev.aborted ? '[durduruldu — sebep: ' + (ev.reason || 'sebep belirtilmedi') + ']' : '(tamamlandı)');
6107
6243
  bcRefreshPreview();
6108
6244
  if (ideModeOn() && els.bcInput) els.bcInput.focus();
6109
6245
  break;
@@ -291,6 +291,13 @@ body.browser-open #topbar { width: auto; }
291
291
  #eyeBtn:not(.on) { opacity: 0.55; }
292
292
  #eyeBtn.on { background: var(--accent-dim); color: var(--accent); }
293
293
 
294
+ /* internet göstergesi (topbar sağ uç): bağlı = YEŞİL wifi, kopuk = KIRMIZI */
295
+ #netDot { cursor: default; }
296
+ #netDot.net-on { color: #22c55e; }
297
+ #netDot.net-off { color: #ef4444; }
298
+ #netDot.net-off .net-slash { display: block; }
299
+ #netDot.net-on .net-slash { display: none; }
300
+
294
301
  /* #19 sağ panel (paralel ajan konsolu) aç/kapa */
295
302
  #railBtn.on { background: var(--accent-dim); color: var(--accent); }
296
303
  body.rail-hidden #rail { display: none; }
@@ -759,6 +766,34 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
759
766
  padding: 10px 14px;
760
767
  }
761
768
 
769
+ /* durdurma/iptal notu: sebep zorunlu — nötr, sönük ton */
770
+ .msg-sys {
771
+ border: 1px dashed var(--border);
772
+ background: var(--panel);
773
+ color: var(--muted);
774
+ border-radius: 10px;
775
+ padding: 8px 12px;
776
+ font-size: 12.5px;
777
+ word-break: break-word;
778
+ }
779
+
780
+ /* Obscura kurulum ilerleme çubuğu (Ayarlar → Web Arama) */
781
+ .oc-prog { margin-top: 10px; }
782
+ .oc-prog-bar {
783
+ height: 9px;
784
+ background: var(--accent-dim);
785
+ border: 1px solid var(--border);
786
+ border-radius: 6px;
787
+ overflow: hidden;
788
+ }
789
+ .oc-prog-fill {
790
+ height: 100%;
791
+ width: 0%;
792
+ background: var(--accent);
793
+ transition: width 0.25s ease;
794
+ }
795
+ .oc-prog-text { margin-top: 5px; font-size: 12px; color: var(--muted); }
796
+
762
797
  /* markdown */
763
798
  .md p { margin: 0 0 8px; }
764
799
  .md p:last-child { margin-bottom: 0; }
@@ -1773,14 +1808,16 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1773
1808
  flex-shrink: 0;
1774
1809
  }
1775
1810
 
1776
- /* tek tıkla model (OpenCode Zen) hero butonu */
1811
+ /* tek tıkla model (OpenCode Zen) hero butonu — buton ortada */
1777
1812
  .zen-hero {
1778
1813
  padding: 10px 12px;
1779
1814
  border: 1px dashed var(--border);
1780
1815
  border-radius: 10px;
1781
1816
  margin-bottom: 12px;
1782
1817
  background: var(--panel);
1818
+ text-align: center;
1783
1819
  }
1820
+ .zen-hero .sub { text-align: left; }
1784
1821
  .zen-hero .btn { min-width: 220px; }
1785
1822
  .zen-hero .btn .zen-load {
1786
1823
  display: inline-block;
@@ -271,18 +271,86 @@ test('#5 eşzamanlılık limiti: fazlası kuyrukta bekler, slot boşalınca FIFO
271
271
  assert.deepEqual(st, ['queued', 'queued', 'running', 'running']);
272
272
  assert.strictEqual(sent.length, 2);
273
273
 
274
- /* slot açılır → sıradaki queued iş running olur ve gönderilir */
274
+ /* slot açılır → sıradaki queued iş running olur ve gönderilir.
275
+ _bgFinish('aborted') artık üst sohbete [ARKA PLAN İPTAL + SEBEP] raporu da düşürür. */
275
276
  eng._bgFinish(ids[0], 'aborted');
276
277
  await sleep(30);
277
278
  const queued = [...eng._bgJobs.values()].filter((j) => j.status === 'queued').length;
278
279
  const running = [...eng._bgJobs.values()].filter((j) => j.status === 'running').length;
279
280
  assert.strictEqual(queued, 1);
280
281
  assert.strictEqual(running, 2);
281
- assert.strictEqual(sent.length, 3);
282
+ /* 3 gönderim = kuyruktan başlayan iş + 1 rapor = üst sohbete düşen iptal sebebi */
283
+ assert.strictEqual(sent.length, 4);
284
+ assert.ok(eng._pendingReports.length === 0, 'iptal raporu üst sohbete düştü');
282
285
  });
283
286
 
284
- /* ---------- /stop anahtarı ---------- */test('stopAll: koşan turları keser, paralel ajanı aborted işaretler', async () => {
287
+ /* ---------- İPTAL SEBEBİ DİSİPLİNİ ---------- */
288
+
289
+ test('iptal SEBEBİ zorunlu: interrupt sebebi iş kaydına ve üst sohbete yazar', async () => {
285
290
  const { eng } = tmpEngine();
291
+ const sent = [];
292
+ eng.send = (sid, payload) => { sent.push({ sid, text: String((payload && payload.text) || '') }); return true; };
293
+ const r = eng.runBackground('pr', 'koşan iş', 'Sebep İş');
294
+ const id = r.backgroundId;
295
+ const c = new AbortController();
296
+ eng.ctrls.set(id, c);
297
+
298
+ eng.interrupt(id, 'kullanıcı panelden iptal etti');
299
+ assert.ok(c.signal.aborted, 'ctrl kesildi');
300
+ assert.strictEqual(eng._abortReasons.get(id), 'kullanıcı panelden iptal etti', 'sebep haritada');
301
+
302
+ /* _run catch'inin yaptığı iş: haritadan sebebi okuyup _bgFinish'e verir */
303
+ const why = eng._abortReasons.get(id);
304
+ eng._abortReasons.delete(id);
305
+ eng._bgFinish(id, 'aborted', why);
306
+
307
+ const j = eng._bgJobs.get(id);
308
+ assert.strictEqual(j.status, 'aborted');
309
+ assert.strictEqual(j.error, 'kullanıcı panelden iptal etti', 'sebep iş kaydına yazıldı');
310
+ const rep = sent.find((x) => x.sid === 'pr' && x.text.includes('[ARKA PLAN İPTAL'));
311
+ assert.ok(rep, 'üst sohbete İPTAL raporu düştü');
312
+ assert.ok(/Sebep: kullanıcı panelden/.test(rep.text), 'rapor SEBEPİ içerir');
313
+ });
314
+
315
+ test('kuyruktaki ajan interrupt ile SEBEPİYLE kesilir', async () => {
316
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-qc-'));
317
+ const eng = new Engine({}, { sessionsDir: dir, emit: () => {} });
318
+ eng._bgLimit = 0; // yapıcı min 1 kelepçeler; sıfırla → her iş queued kalır
319
+ eng.send = () => true;
320
+ const r = eng.runBackground('pq2', 'kuyruk işi', 'Kuyruk İş');
321
+ await sleep(80); // admit → bgLimit 0 → queued
322
+ assert.strictEqual(eng._bgJobs.get(r.backgroundId).status, 'queued');
323
+
324
+ assert.strictEqual(eng.interrupt(r.backgroundId, 'sıra iptal edildi'), true);
325
+ const j = eng._bgJobs.get(r.backgroundId);
326
+ assert.strictEqual(j.status, 'aborted');
327
+ assert.strictEqual(j.error, 'sıra iptal edildi', 'kuyruktaki işin iptal sebebi kayda geçti');
328
+ eng.deleteSession(r.backgroundId);
329
+ });
330
+
331
+ test('task_cancel reason ZORUNLU: sebep iş kaydına ve note mesajına düşer', async () => {
332
+ const { eng } = tmpEngine();
333
+ const r = eng.runBackground('pc', 'iş', 'İptal Edilen');
334
+ const id = r.backgroundId;
335
+ const c = new AbortController();
336
+ eng.ctrls.set(id, c);
337
+
338
+ const out = JSON.parse(await eng._execTool('task_cancel', { id, reason: 'yanlış kaynağa baktı' }, {}));
339
+ assert.ok(out.ok, 'cancel başarılı');
340
+ assert.ok(/yanlış kaynağa baktı/.test(out.note), 'note ceza sebep içerir');
341
+ assert.strictEqual(eng._abortReasons.get(id), 'yanlış kaynağa baktı');
342
+ assert.ok(c.signal.aborted);
343
+
344
+ /* reason verilmezse varsayılan sebep yine kayda düşer */
345
+ const id2 = eng.runBackground('pc', 'iş 2', 'İptal 2').backgroundId;
346
+ eng.ctrls.set(id2, new AbortController());
347
+ const out2 = JSON.parse(await eng._execTool('task_cancel', { id: id2 }, {}));
348
+ assert.ok(out2.ok);
349
+ assert.ok(eng._abortReasons.get(id2).includes('task_cancel'), 'varsayılan sebep CEO kaynağını söyler');
350
+ });
351
+
352
+ /* ---------- /stop anahtarı ---------- */
353
+ test('stopAll: koşan turları keser, paralel ajanı aborted işaretler', async () => { const { eng } = tmpEngine();
286
354
  const r = eng.runBackground('pS', 'koşan iş', 'Koşu');
287
355
  const id = r.backgroundId;
288
356
 
@@ -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
+ });